diff --git a/src/roamhq/.fern/metadata.json b/src/roamhq/.fern/metadata.json
index e6bd928..7343555 100644
--- a/src/roamhq/.fern/metadata.json
+++ b/src/roamhq/.fern/metadata.json
@@ -6,7 +6,7 @@
"package_name": "roamhq",
"client_class_name": "RoamClient"
},
- "originGitCommit": "e226955133092595744390682ba317d4ee33f224",
+ "originGitCommit": "3af58388f85aa0afafd3cdfef05c3d3fd959566a",
"originGitCommitIsDirty": false,
"invokedBy": "ci",
"ciProvider": "github"
diff --git a/src/roamhq/__init__.py b/src/roamhq/__init__.py
index 0d7f0a2..7098887 100644
--- a/src/roamhq/__init__.py
+++ b/src/roamhq/__init__.py
@@ -29,6 +29,7 @@
GroupMember,
GroupMemberRole,
GroupType,
+ GuestBadge,
LobbyBooking,
LobbyBookingHost,
LobbyBookingInvitee,
@@ -53,13 +54,17 @@
UserAuditLog,
UserAuditLogPlatform,
UserStatus,
+ UserStatusBubbleResponse,
+ UserStatusBubbleResponseStatusBubble,
UserType,
- UserWillReturn,
Webhook,
+ WebhookDestination,
+ WebhookDestinationType,
WebhookEvent,
WebhookSubscriptionFilter,
WebhookSubscriptionFilterChatType,
WebhookSubscriptionFilterStatus,
+ WillReturn,
)
from .errors import (
BadRequestError,
@@ -80,6 +85,7 @@
conversation,
group,
groups,
+ guest_badges,
item,
lobby,
magicast,
@@ -156,6 +162,7 @@
MembersGroupResponse,
)
from .groups import GroupsListResponseItem
+ from .guest_badges import GuestBadgeListResponse, GuestBadgeRevokeResponse
from .lobby import ListBookingsLobbyResponse, ListLobbyResponse, ListLobbyResponseLobbiesItem
from .magicast import ListMagicastResponse
from .magicasts import MagicastShareLinkResponse
@@ -184,12 +191,19 @@
from .token import InfoTokenResponse, InfoTokenResponseBot, InfoTokenResponseRoam, InfoTokenResponseUser
from .user import ListUserResponse
from .user_audit_log import ListUserAuditLogResponse
- from .users import UserActivityListResponse
+ from .users import (
+ UserActivityListResponse,
+ UserStatusSetRequestWillReturn,
+ UserStatusSetResponse,
+ UserStatusSetResponseStatus,
+ )
from .webhook import (
DeliveriesWebhookResponse,
DeliveriesWebhookResponseDeliveriesItem,
ListWebhookResponse,
ListWebhookResponseWebhooksItem,
+ WebhookSubscriptionRequestDestination,
+ WebhookSubscriptionRequestDestinationType,
WebhookSubscriptionRequestEvent,
)
_dynamic_imports: typing.Dict[str, str] = {
@@ -237,6 +251,9 @@
"GroupMemberRole": ".types",
"GroupType": ".types",
"GroupsListResponseItem": ".groups",
+ "GuestBadge": ".types",
+ "GuestBadgeListResponse": ".guest_badges",
+ "GuestBadgeRevokeResponse": ".guest_badges",
"HistoryChatResponse": ".chat",
"InfoMeetingResponse": ".meeting",
"InfoMeetingResponseChaptersItem": ".meeting",
@@ -342,20 +359,30 @@
"UserAuditLog": ".types",
"UserAuditLogPlatform": ".types",
"UserStatus": ".types",
+ "UserStatusBubbleResponse": ".types",
+ "UserStatusBubbleResponseStatusBubble": ".types",
+ "UserStatusSetRequestWillReturn": ".users",
+ "UserStatusSetResponse": ".users",
+ "UserStatusSetResponseStatus": ".users",
"UserType": ".types",
- "UserWillReturn": ".types",
"Webhook": ".types",
+ "WebhookDestination": ".types",
+ "WebhookDestinationType": ".types",
"WebhookEvent": ".types",
"WebhookSubscriptionFilter": ".types",
"WebhookSubscriptionFilterChatType": ".types",
"WebhookSubscriptionFilterStatus": ".types",
+ "WebhookSubscriptionRequestDestination": ".webhook",
+ "WebhookSubscriptionRequestDestinationType": ".webhook",
"WebhookSubscriptionRequestEvent": ".webhook",
+ "WillReturn": ".types",
"asset": ".asset",
"calendar": ".calendar",
"chat": ".chat",
"conversation": ".conversation",
"group": ".group",
"groups": ".groups",
+ "guest_badges": ".guest_badges",
"item": ".item",
"lobby": ".lobby",
"magicast": ".magicast",
@@ -438,6 +465,9 @@ def __dir__():
"GroupMemberRole",
"GroupType",
"GroupsListResponseItem",
+ "GuestBadge",
+ "GuestBadgeListResponse",
+ "GuestBadgeRevokeResponse",
"HistoryChatResponse",
"InfoMeetingResponse",
"InfoMeetingResponseChaptersItem",
@@ -543,20 +573,30 @@ def __dir__():
"UserAuditLog",
"UserAuditLogPlatform",
"UserStatus",
+ "UserStatusBubbleResponse",
+ "UserStatusBubbleResponseStatusBubble",
+ "UserStatusSetRequestWillReturn",
+ "UserStatusSetResponse",
+ "UserStatusSetResponseStatus",
"UserType",
- "UserWillReturn",
"Webhook",
+ "WebhookDestination",
+ "WebhookDestinationType",
"WebhookEvent",
"WebhookSubscriptionFilter",
"WebhookSubscriptionFilterChatType",
"WebhookSubscriptionFilterStatus",
+ "WebhookSubscriptionRequestDestination",
+ "WebhookSubscriptionRequestDestinationType",
"WebhookSubscriptionRequestEvent",
+ "WillReturn",
"asset",
"calendar",
"chat",
"conversation",
"group",
"groups",
+ "guest_badges",
"item",
"lobby",
"magicast",
diff --git a/src/roamhq/client.py b/src/roamhq/client.py
index 691744b..0cbc79a 100644
--- a/src/roamhq/client.py
+++ b/src/roamhq/client.py
@@ -16,6 +16,7 @@
from .conversation.client import AsyncConversationClient, ConversationClient
from .group.client import AsyncGroupClient, GroupClient
from .groups.client import AsyncGroupsClient, GroupsClient
+ from .guest_badges.client import AsyncGuestBadgesClient, GuestBadgesClient
from .item.client import AsyncItemClient, ItemClient
from .lobby.client import AsyncLobbyClient, LobbyClient
from .magicast.client import AsyncMagicastClient, MagicastClient
@@ -136,6 +137,7 @@ def __init__(
self._magicasts: typing.Optional[MagicastsClient] = None
self._group: typing.Optional[GroupClient] = None
self._groups: typing.Optional[GroupsClient] = None
+ self._guest_badges: typing.Optional[GuestBadgesClient] = None
self._token: typing.Optional[TokenClient] = None
self._webhook: typing.Optional[WebhookClient] = None
@@ -275,6 +277,14 @@ def groups(self):
self._groups = GroupsClient(client_wrapper=self._client_wrapper)
return self._groups
+ @property
+ def guest_badges(self):
+ if self._guest_badges is None:
+ from .guest_badges.client import GuestBadgesClient # noqa: E402
+
+ self._guest_badges = GuestBadgesClient(client_wrapper=self._client_wrapper)
+ return self._guest_badges
+
@property
def token(self):
if self._token is None:
@@ -418,6 +428,7 @@ def __init__(
self._magicasts: typing.Optional[AsyncMagicastsClient] = None
self._group: typing.Optional[AsyncGroupClient] = None
self._groups: typing.Optional[AsyncGroupsClient] = None
+ self._guest_badges: typing.Optional[AsyncGuestBadgesClient] = None
self._token: typing.Optional[AsyncTokenClient] = None
self._webhook: typing.Optional[AsyncWebhookClient] = None
@@ -557,6 +568,14 @@ def groups(self):
self._groups = AsyncGroupsClient(client_wrapper=self._client_wrapper)
return self._groups
+ @property
+ def guest_badges(self):
+ if self._guest_badges is None:
+ from .guest_badges.client import AsyncGuestBadgesClient # noqa: E402
+
+ self._guest_badges = AsyncGuestBadgesClient(client_wrapper=self._client_wrapper)
+ return self._guest_badges
+
@property
def token(self):
if self._token is None:
diff --git a/src/roamhq/core/client_wrapper.py b/src/roamhq/core/client_wrapper.py
index ed30d24..18846c9 100644
--- a/src/roamhq/core/client_wrapper.py
+++ b/src/roamhq/core/client_wrapper.py
@@ -37,6 +37,7 @@ def get_headers(self) -> typing.Dict[str, str]:
import platform
headers: typing.Dict[str, str] = {
+ "User-Agent": "roamhq/0.1.1",
"X-Fern-Language": "Python",
"X-Fern-Runtime": f"python/{platform.python_version()}",
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
diff --git a/src/roamhq/group/client.py b/src/roamhq/group/client.py
index 7b95267..2b75723 100644
--- a/src/roamhq/group/client.py
+++ b/src/roamhq/group/client.py
@@ -151,6 +151,9 @@ def create(
that capability.
Groups require at least one member. Users can be specified by user ID or email address.
+ Unrecognized emails are invited as group members only — they do not receive a
+ [Guest Badge](https://developer.ro.am/docs/guides/guest-badges) unless you also call
+ [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create).
**Required scope:** `group:write`
@@ -360,6 +363,8 @@ def add(
Members can be specified by user ID or email address. Each member must be assigned a role (member or admin).
+ Adding an unrecognized email does **not** grant a [Guest Badge](https://developer.ro.am/docs/guides/guest-badges). Use [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) first if the person is not a workspace member.
+
Apps may add members to a group if one of the following conditions is true:
1. It is a public group in their Roam.
2. They are a member of the group.
@@ -656,6 +661,9 @@ async def create(
that capability.
Groups require at least one member. Users can be specified by user ID or email address.
+ Unrecognized emails are invited as group members only — they do not receive a
+ [Guest Badge](https://developer.ro.am/docs/guides/guest-badges) unless you also call
+ [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create).
**Required scope:** `group:write`
@@ -897,6 +905,8 @@ async def add(
Members can be specified by user ID or email address. Each member must be assigned a role (member or admin).
+ Adding an unrecognized email does **not** grant a [Guest Badge](https://developer.ro.am/docs/guides/guest-badges). Use [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) first if the person is not a workspace member.
+
Apps may add members to a group if one of the following conditions is true:
1. It is a public group in their Roam.
2. They are a member of the group.
diff --git a/src/roamhq/group/raw_client.py b/src/roamhq/group/raw_client.py
index 4f425de..058092f 100644
--- a/src/roamhq/group/raw_client.py
+++ b/src/roamhq/group/raw_client.py
@@ -316,6 +316,9 @@ def create(
that capability.
Groups require at least one member. Users can be specified by user ID or email address.
+ Unrecognized emails are invited as group members only — they do not receive a
+ [Guest Badge](https://developer.ro.am/docs/guides/guest-badges) unless you also call
+ [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create).
**Required scope:** `group:write`
@@ -798,6 +801,8 @@ def add(
Members can be specified by user ID or email address. Each member must be assigned a role (member or admin).
+ Adding an unrecognized email does **not** grant a [Guest Badge](https://developer.ro.am/docs/guides/guest-badges). Use [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) first if the person is not a workspace member.
+
Apps may add members to a group if one of the following conditions is true:
1. It is a public group in their Roam.
2. They are a member of the group.
@@ -1427,6 +1432,9 @@ async def create(
that capability.
Groups require at least one member. Users can be specified by user ID or email address.
+ Unrecognized emails are invited as group members only — they do not receive a
+ [Guest Badge](https://developer.ro.am/docs/guides/guest-badges) unless you also call
+ [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create).
**Required scope:** `group:write`
@@ -1911,6 +1919,8 @@ async def add(
Members can be specified by user ID or email address. Each member must be assigned a role (member or admin).
+ Adding an unrecognized email does **not** grant a [Guest Badge](https://developer.ro.am/docs/guides/guest-badges). Use [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) first if the person is not a workspace member.
+
Apps may add members to a group if one of the following conditions is true:
1. It is a public group in their Roam.
2. They are a member of the group.
diff --git a/src/roamhq/guest_badges/__init__.py b/src/roamhq/guest_badges/__init__.py
new file mode 100644
index 0000000..975dc59
--- /dev/null
+++ b/src/roamhq/guest_badges/__init__.py
@@ -0,0 +1,36 @@
+# This file was auto-generated by Fern from our API Definition.
+
+# isort: skip_file
+
+from __future__ import annotations
+
+import typing
+from importlib import import_module
+
+if typing.TYPE_CHECKING:
+ from .types import GuestBadgeListResponse, GuestBadgeRevokeResponse
+_dynamic_imports: typing.Dict[str, str] = {"GuestBadgeListResponse": ".types", "GuestBadgeRevokeResponse": ".types"}
+
+
+def __getattr__(attr_name: str) -> typing.Any:
+ module_name = _dynamic_imports.get(attr_name)
+ if module_name is None:
+ raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
+ try:
+ module = import_module(module_name, __package__)
+ if module_name == f".{attr_name}":
+ return module
+ else:
+ return getattr(module, attr_name)
+ except ImportError as e:
+ raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
+ except AttributeError as e:
+ raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
+
+
+def __dir__():
+ lazy_attrs = list(_dynamic_imports.keys())
+ return sorted(lazy_attrs)
+
+
+__all__ = ["GuestBadgeListResponse", "GuestBadgeRevokeResponse"]
diff --git a/src/roamhq/guest_badges/client.py b/src/roamhq/guest_badges/client.py
new file mode 100644
index 0000000..b671804
--- /dev/null
+++ b/src/roamhq/guest_badges/client.py
@@ -0,0 +1,651 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ..core.request_options import RequestOptions
+from ..types.guest_badge import GuestBadge
+from .raw_client import AsyncRawGuestBadgesClient, RawGuestBadgesClient
+from .types.guest_badge_list_response import GuestBadgeListResponse
+from .types.guest_badge_revoke_response import GuestBadgeRevokeResponse
+
+# this is used as the default value for optional parameters
+OMIT = typing.cast(typing.Any, ...)
+
+
+class GuestBadgesClient:
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
+ self._raw_client = RawGuestBadgesClient(client_wrapper=client_wrapper)
+
+ @property
+ def with_raw_response(self) -> RawGuestBadgesClient:
+ """
+ Retrieves a raw implementation of this client that returns raw responses.
+
+ Returns
+ -------
+ RawGuestBadgesClient
+ """
+ return self._raw_client
+
+ def guest_badge_create(
+ self,
+ *,
+ email: str,
+ host_user_id: typing.Optional[str] = OMIT,
+ visit_permission: typing.Optional[bool] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> GuestBadge:
+ """
+ Grant a Guest Badge so an email that is **not** a workspace member can
+ visit a host in Roam.
+
+ This is **not** an [On-Air event guest](https://developer.ro.am/docs/onair-api/on-air-api). It is
+ also **not** implied by [`group.add`](https://developer.ro.am/docs/api/group-add): adding an email
+ to a group does not mint a badge or send the invite. Typical onboarding is
+ `guest.badge.create` then `group.add`.
+
+ Repeating create for the same host and email returns the existing badge
+ (`visitPermission` is **not** updated) and does not re-send the invite.
+ To flip on-map access after create, use
+ [`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update).
+
+ **Access:** Organization and Personal.
+ Organization tokens require `hostUserId`. Personal tokens default to the
+ token owner; naming a different host returns `403` `access_mode_not_supported`.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only. Must not be a workspace member.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email — the same convention as
+ [`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+ Required for organization tokens. Optional for personal tokens
+ (defaults to the token owner).
+
+ visit_permission : typing.Optional[bool]
+ Whether the guest may visit the host on the map. Defaults to
+ `true`. Ignored on an idempotent retry of an existing grant
+ — use [`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update)
+ to change it.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ GuestBadge
+ Badge created, or the existing grant returned.
+
+ Examples
+ --------
+ from roamhq import RoamClient
+
+ client = RoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+ client.guest_badges.guest_badge_create(
+ email="alex@client.example",
+ host_user_id="3f1c0b2a-8d4e-4c91-9a7b-2e6f1d8c0a11",
+ )
+ """
+ _response = self._raw_client.guest_badge_create(
+ email=email, host_user_id=host_user_id, visit_permission=visit_permission, request_options=request_options
+ )
+ return _response.data
+
+ def guest_badge_list(
+ self,
+ *,
+ email: typing.Optional[str] = None,
+ host_user_id: typing.Optional[str] = None,
+ limit: typing.Optional[int] = None,
+ cursor: typing.Optional[str] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> GuestBadgeListResponse:
+ """
+ List issued Guest Badges.
+
+ Organization tokens return every issued badge in the workspace. Personal
+ tokens return only badges the token owner issued. This is the issued
+ (host) view — the same rows `guest.badge.create` returns — not the
+ guest's hidden-inbox view.
+
+ Filter with `email` (alias-aware) and/or `hostUserId` (UUID or member
+ email). Paginate with `limit` / `cursor` (default 50, max 100). Results
+ are sorted by `(hostUserId, email)`.
+
+ Organization keys that only have `guest:write` must also request
+ `guest:read` to call list. Personal Access Tokens with `pat:guests:write`
+ already include `guest:read`.
+
+ **Access:** Organization and Personal.
+ Personal tokens naming a different host return `403`
+ `access_mode_not_supported`.
+
+ **Required scope:** `guest:read`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : typing.Optional[str]
+ Guest email. ASCII only. Matches the stored address and its verified
+ domain aliases.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email — the same convention as
+ [`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+ Archived hosts may be named (they typically have no remaining grants).
+ Personal tokens may only pass the token owner.
+
+ limit : typing.Optional[int]
+ Number of badges to return per page (default 50, max 100).
+
+ cursor : typing.Optional[str]
+ Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ GuestBadgeListResponse
+ Issued Guest Badges for this page.
+
+ Examples
+ --------
+ from roamhq import RoamClient
+
+ client = RoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+ client.guest_badges.guest_badge_list()
+ """
+ _response = self._raw_client.guest_badge_list(
+ email=email, host_user_id=host_user_id, limit=limit, cursor=cursor, request_options=request_options
+ )
+ return _response.data
+
+ def guest_badge_update(
+ self,
+ *,
+ email: str,
+ visit_permission: bool,
+ host_user_id: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> GuestBadge:
+ """
+ Update `visitPermission` on an existing Guest Badge.
+
+ [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) is idempotent and
+ does **not** change `visitPermission` on an existing grant. Use this
+ endpoint to flip on-map visit access after create.
+
+ If only one host in the workspace has granted this email, `hostUserId`
+ may be omitted. If several hosts have, pass `hostUserId` to pick which
+ grant to update (`400` `missing_parameter` otherwise). Same-host alias
+ rows are updated together.
+
+ Personal tokens can only update badges they issued. Naming another host
+ is `403` `access_mode_not_supported`; omitting `hostUserId` when the
+ token owner has no matching grant is `404` `not_found`.
+
+ **Access:** Organization and Personal.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only.
+
+ visit_permission : bool
+ Whether the guest may visit the host on the map.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email. Required when more than one
+ host has granted this email. Optional for a unique grant, and
+ for personal tokens (defaults to the token owner).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ GuestBadge
+ The updated Guest Badge.
+
+ Examples
+ --------
+ from roamhq import RoamClient
+
+ client = RoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+ client.guest_badges.guest_badge_update(
+ email="alex@client.example",
+ host_user_id="3f1c0b2a-8d4e-4c91-9a7b-2e6f1d8c0a11",
+ visit_permission=False,
+ )
+ """
+ _response = self._raw_client.guest_badge_update(
+ email=email, visit_permission=visit_permission, host_user_id=host_user_id, request_options=request_options
+ )
+ return _response.data
+
+ def guest_badge_revoke(
+ self,
+ *,
+ email: str,
+ host_user_id: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> GuestBadgeRevokeResponse:
+ """
+ Revoke Guest Badge(s) for an email.
+
+ If only one host in the workspace has granted this email, `hostUserId` may
+ be omitted. If several hosts have, pass `hostUserId` to pick which grant
+ to revoke (`400` `missing_parameter` otherwise). Same-host alias rows are
+ all revoked together.
+
+ Returns `{ "revoked": true }` when a matching grant was found and deleted,
+ or `{ "revoked": false }` when there was nothing to revoke (already gone,
+ including after the host was archived — archiving a member deletes the
+ badges they granted). Naming an archived host does not 404.
+
+ Personal tokens can only revoke badges they issued. Naming another host is
+ `403` `access_mode_not_supported`; omitting `hostUserId` when only another
+ host granted the email is a no-op (`revoked: false`).
+
+ **Access:** Organization and Personal.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email. Required when more than one
+ host has granted this email. Optional for a unique grant, and
+ for personal tokens (defaults to the token owner).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ GuestBadgeRevokeResponse
+ Revoke attempted. `revoked` is true only when a matching grant was deleted.
+
+ Examples
+ --------
+ from roamhq import RoamClient
+
+ client = RoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+ client.guest_badges.guest_badge_revoke(
+ email="alex@client.example",
+ )
+ """
+ _response = self._raw_client.guest_badge_revoke(
+ email=email, host_user_id=host_user_id, request_options=request_options
+ )
+ return _response.data
+
+
+class AsyncGuestBadgesClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._raw_client = AsyncRawGuestBadgesClient(client_wrapper=client_wrapper)
+
+ @property
+ def with_raw_response(self) -> AsyncRawGuestBadgesClient:
+ """
+ Retrieves a raw implementation of this client that returns raw responses.
+
+ Returns
+ -------
+ AsyncRawGuestBadgesClient
+ """
+ return self._raw_client
+
+ async def guest_badge_create(
+ self,
+ *,
+ email: str,
+ host_user_id: typing.Optional[str] = OMIT,
+ visit_permission: typing.Optional[bool] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> GuestBadge:
+ """
+ Grant a Guest Badge so an email that is **not** a workspace member can
+ visit a host in Roam.
+
+ This is **not** an [On-Air event guest](https://developer.ro.am/docs/onair-api/on-air-api). It is
+ also **not** implied by [`group.add`](https://developer.ro.am/docs/api/group-add): adding an email
+ to a group does not mint a badge or send the invite. Typical onboarding is
+ `guest.badge.create` then `group.add`.
+
+ Repeating create for the same host and email returns the existing badge
+ (`visitPermission` is **not** updated) and does not re-send the invite.
+ To flip on-map access after create, use
+ [`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update).
+
+ **Access:** Organization and Personal.
+ Organization tokens require `hostUserId`. Personal tokens default to the
+ token owner; naming a different host returns `403` `access_mode_not_supported`.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only. Must not be a workspace member.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email — the same convention as
+ [`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+ Required for organization tokens. Optional for personal tokens
+ (defaults to the token owner).
+
+ visit_permission : typing.Optional[bool]
+ Whether the guest may visit the host on the map. Defaults to
+ `true`. Ignored on an idempotent retry of an existing grant
+ — use [`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update)
+ to change it.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ GuestBadge
+ Badge created, or the existing grant returned.
+
+ Examples
+ --------
+ import asyncio
+
+ from roamhq import AsyncRoamClient
+
+ client = AsyncRoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.guest_badges.guest_badge_create(
+ email="alex@client.example",
+ host_user_id="3f1c0b2a-8d4e-4c91-9a7b-2e6f1d8c0a11",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.guest_badge_create(
+ email=email, host_user_id=host_user_id, visit_permission=visit_permission, request_options=request_options
+ )
+ return _response.data
+
+ async def guest_badge_list(
+ self,
+ *,
+ email: typing.Optional[str] = None,
+ host_user_id: typing.Optional[str] = None,
+ limit: typing.Optional[int] = None,
+ cursor: typing.Optional[str] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> GuestBadgeListResponse:
+ """
+ List issued Guest Badges.
+
+ Organization tokens return every issued badge in the workspace. Personal
+ tokens return only badges the token owner issued. This is the issued
+ (host) view — the same rows `guest.badge.create` returns — not the
+ guest's hidden-inbox view.
+
+ Filter with `email` (alias-aware) and/or `hostUserId` (UUID or member
+ email). Paginate with `limit` / `cursor` (default 50, max 100). Results
+ are sorted by `(hostUserId, email)`.
+
+ Organization keys that only have `guest:write` must also request
+ `guest:read` to call list. Personal Access Tokens with `pat:guests:write`
+ already include `guest:read`.
+
+ **Access:** Organization and Personal.
+ Personal tokens naming a different host return `403`
+ `access_mode_not_supported`.
+
+ **Required scope:** `guest:read`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : typing.Optional[str]
+ Guest email. ASCII only. Matches the stored address and its verified
+ domain aliases.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email — the same convention as
+ [`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+ Archived hosts may be named (they typically have no remaining grants).
+ Personal tokens may only pass the token owner.
+
+ limit : typing.Optional[int]
+ Number of badges to return per page (default 50, max 100).
+
+ cursor : typing.Optional[str]
+ Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ GuestBadgeListResponse
+ Issued Guest Badges for this page.
+
+ Examples
+ --------
+ import asyncio
+
+ from roamhq import AsyncRoamClient
+
+ client = AsyncRoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.guest_badges.guest_badge_list()
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.guest_badge_list(
+ email=email, host_user_id=host_user_id, limit=limit, cursor=cursor, request_options=request_options
+ )
+ return _response.data
+
+ async def guest_badge_update(
+ self,
+ *,
+ email: str,
+ visit_permission: bool,
+ host_user_id: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> GuestBadge:
+ """
+ Update `visitPermission` on an existing Guest Badge.
+
+ [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) is idempotent and
+ does **not** change `visitPermission` on an existing grant. Use this
+ endpoint to flip on-map visit access after create.
+
+ If only one host in the workspace has granted this email, `hostUserId`
+ may be omitted. If several hosts have, pass `hostUserId` to pick which
+ grant to update (`400` `missing_parameter` otherwise). Same-host alias
+ rows are updated together.
+
+ Personal tokens can only update badges they issued. Naming another host
+ is `403` `access_mode_not_supported`; omitting `hostUserId` when the
+ token owner has no matching grant is `404` `not_found`.
+
+ **Access:** Organization and Personal.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only.
+
+ visit_permission : bool
+ Whether the guest may visit the host on the map.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email. Required when more than one
+ host has granted this email. Optional for a unique grant, and
+ for personal tokens (defaults to the token owner).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ GuestBadge
+ The updated Guest Badge.
+
+ Examples
+ --------
+ import asyncio
+
+ from roamhq import AsyncRoamClient
+
+ client = AsyncRoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.guest_badges.guest_badge_update(
+ email="alex@client.example",
+ host_user_id="3f1c0b2a-8d4e-4c91-9a7b-2e6f1d8c0a11",
+ visit_permission=False,
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.guest_badge_update(
+ email=email, visit_permission=visit_permission, host_user_id=host_user_id, request_options=request_options
+ )
+ return _response.data
+
+ async def guest_badge_revoke(
+ self,
+ *,
+ email: str,
+ host_user_id: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> GuestBadgeRevokeResponse:
+ """
+ Revoke Guest Badge(s) for an email.
+
+ If only one host in the workspace has granted this email, `hostUserId` may
+ be omitted. If several hosts have, pass `hostUserId` to pick which grant
+ to revoke (`400` `missing_parameter` otherwise). Same-host alias rows are
+ all revoked together.
+
+ Returns `{ "revoked": true }` when a matching grant was found and deleted,
+ or `{ "revoked": false }` when there was nothing to revoke (already gone,
+ including after the host was archived — archiving a member deletes the
+ badges they granted). Naming an archived host does not 404.
+
+ Personal tokens can only revoke badges they issued. Naming another host is
+ `403` `access_mode_not_supported`; omitting `hostUserId` when only another
+ host granted the email is a no-op (`revoked: false`).
+
+ **Access:** Organization and Personal.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email. Required when more than one
+ host has granted this email. Optional for a unique grant, and
+ for personal tokens (defaults to the token owner).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ GuestBadgeRevokeResponse
+ Revoke attempted. `revoked` is true only when a matching grant was deleted.
+
+ Examples
+ --------
+ import asyncio
+
+ from roamhq import AsyncRoamClient
+
+ client = AsyncRoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.guest_badges.guest_badge_revoke(
+ email="alex@client.example",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.guest_badge_revoke(
+ email=email, host_user_id=host_user_id, request_options=request_options
+ )
+ return _response.data
diff --git a/src/roamhq/guest_badges/raw_client.py b/src/roamhq/guest_badges/raw_client.py
new file mode 100644
index 0000000..d22b2d4
--- /dev/null
+++ b/src/roamhq/guest_badges/raw_client.py
@@ -0,0 +1,1344 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+from json.decoder import JSONDecodeError
+
+from ..core.api_error import ApiError
+from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
+from ..core.http_response import AsyncHttpResponse, HttpResponse
+from ..core.parse_error import ParsingError
+from ..core.pydantic_utilities import parse_obj_as
+from ..core.request_options import RequestOptions
+from ..errors.bad_request_error import BadRequestError
+from ..errors.forbidden_error import ForbiddenError
+from ..errors.internal_server_error import InternalServerError
+from ..errors.method_not_allowed_error import MethodNotAllowedError
+from ..errors.not_found_error import NotFoundError
+from ..errors.too_many_requests_error import TooManyRequestsError
+from ..errors.unauthorized_error import UnauthorizedError
+from ..types.error import Error
+from ..types.guest_badge import GuestBadge
+from .types.guest_badge_list_response import GuestBadgeListResponse
+from .types.guest_badge_revoke_response import GuestBadgeRevokeResponse
+from pydantic import ValidationError
+
+# this is used as the default value for optional parameters
+OMIT = typing.cast(typing.Any, ...)
+
+
+class RawGuestBadgesClient:
+ def __init__(self, *, client_wrapper: SyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ def guest_badge_create(
+ self,
+ *,
+ email: str,
+ host_user_id: typing.Optional[str] = OMIT,
+ visit_permission: typing.Optional[bool] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[GuestBadge]:
+ """
+ Grant a Guest Badge so an email that is **not** a workspace member can
+ visit a host in Roam.
+
+ This is **not** an [On-Air event guest](https://developer.ro.am/docs/onair-api/on-air-api). It is
+ also **not** implied by [`group.add`](https://developer.ro.am/docs/api/group-add): adding an email
+ to a group does not mint a badge or send the invite. Typical onboarding is
+ `guest.badge.create` then `group.add`.
+
+ Repeating create for the same host and email returns the existing badge
+ (`visitPermission` is **not** updated) and does not re-send the invite.
+ To flip on-map access after create, use
+ [`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update).
+
+ **Access:** Organization and Personal.
+ Organization tokens require `hostUserId`. Personal tokens default to the
+ token owner; naming a different host returns `403` `access_mode_not_supported`.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only. Must not be a workspace member.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email — the same convention as
+ [`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+ Required for organization tokens. Optional for personal tokens
+ (defaults to the token owner).
+
+ visit_permission : typing.Optional[bool]
+ Whether the guest may visit the host on the map. Defaults to
+ `true`. Ignored on an idempotent retry of an existing grant
+ — use [`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update)
+ to change it.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[GuestBadge]
+ Badge created, or the existing grant returned.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "guest.badge.create",
+ method="POST",
+ json={
+ "email": email,
+ "hostUserId": host_user_id,
+ "visitPermission": visit_permission,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ GuestBadge,
+ parse_obj_as(
+ type_=GuestBadge, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def guest_badge_list(
+ self,
+ *,
+ email: typing.Optional[str] = None,
+ host_user_id: typing.Optional[str] = None,
+ limit: typing.Optional[int] = None,
+ cursor: typing.Optional[str] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[GuestBadgeListResponse]:
+ """
+ List issued Guest Badges.
+
+ Organization tokens return every issued badge in the workspace. Personal
+ tokens return only badges the token owner issued. This is the issued
+ (host) view — the same rows `guest.badge.create` returns — not the
+ guest's hidden-inbox view.
+
+ Filter with `email` (alias-aware) and/or `hostUserId` (UUID or member
+ email). Paginate with `limit` / `cursor` (default 50, max 100). Results
+ are sorted by `(hostUserId, email)`.
+
+ Organization keys that only have `guest:write` must also request
+ `guest:read` to call list. Personal Access Tokens with `pat:guests:write`
+ already include `guest:read`.
+
+ **Access:** Organization and Personal.
+ Personal tokens naming a different host return `403`
+ `access_mode_not_supported`.
+
+ **Required scope:** `guest:read`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : typing.Optional[str]
+ Guest email. ASCII only. Matches the stored address and its verified
+ domain aliases.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email — the same convention as
+ [`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+ Archived hosts may be named (they typically have no remaining grants).
+ Personal tokens may only pass the token owner.
+
+ limit : typing.Optional[int]
+ Number of badges to return per page (default 50, max 100).
+
+ cursor : typing.Optional[str]
+ Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[GuestBadgeListResponse]
+ Issued Guest Badges for this page.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "guest.badge.list",
+ method="GET",
+ params={
+ "email": email,
+ "hostUserId": host_user_id,
+ "limit": limit,
+ "cursor": cursor,
+ },
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ GuestBadgeListResponse,
+ parse_obj_as(
+ type_=GuestBadgeListResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def guest_badge_update(
+ self,
+ *,
+ email: str,
+ visit_permission: bool,
+ host_user_id: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[GuestBadge]:
+ """
+ Update `visitPermission` on an existing Guest Badge.
+
+ [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) is idempotent and
+ does **not** change `visitPermission` on an existing grant. Use this
+ endpoint to flip on-map visit access after create.
+
+ If only one host in the workspace has granted this email, `hostUserId`
+ may be omitted. If several hosts have, pass `hostUserId` to pick which
+ grant to update (`400` `missing_parameter` otherwise). Same-host alias
+ rows are updated together.
+
+ Personal tokens can only update badges they issued. Naming another host
+ is `403` `access_mode_not_supported`; omitting `hostUserId` when the
+ token owner has no matching grant is `404` `not_found`.
+
+ **Access:** Organization and Personal.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only.
+
+ visit_permission : bool
+ Whether the guest may visit the host on the map.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email. Required when more than one
+ host has granted this email. Optional for a unique grant, and
+ for personal tokens (defaults to the token owner).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[GuestBadge]
+ The updated Guest Badge.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "guest.badge.update",
+ method="POST",
+ json={
+ "email": email,
+ "hostUserId": host_user_id,
+ "visitPermission": visit_permission,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ GuestBadge,
+ parse_obj_as(
+ type_=GuestBadge, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def guest_badge_revoke(
+ self,
+ *,
+ email: str,
+ host_user_id: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[GuestBadgeRevokeResponse]:
+ """
+ Revoke Guest Badge(s) for an email.
+
+ If only one host in the workspace has granted this email, `hostUserId` may
+ be omitted. If several hosts have, pass `hostUserId` to pick which grant
+ to revoke (`400` `missing_parameter` otherwise). Same-host alias rows are
+ all revoked together.
+
+ Returns `{ "revoked": true }` when a matching grant was found and deleted,
+ or `{ "revoked": false }` when there was nothing to revoke (already gone,
+ including after the host was archived — archiving a member deletes the
+ badges they granted). Naming an archived host does not 404.
+
+ Personal tokens can only revoke badges they issued. Naming another host is
+ `403` `access_mode_not_supported`; omitting `hostUserId` when only another
+ host granted the email is a no-op (`revoked: false`).
+
+ **Access:** Organization and Personal.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email. Required when more than one
+ host has granted this email. Optional for a unique grant, and
+ for personal tokens (defaults to the token owner).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[GuestBadgeRevokeResponse]
+ Revoke attempted. `revoked` is true only when a matching grant was deleted.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "guest.badge.revoke",
+ method="POST",
+ json={
+ "email": email,
+ "hostUserId": host_user_id,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ GuestBadgeRevokeResponse,
+ parse_obj_as(
+ type_=GuestBadgeRevokeResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+
+class AsyncRawGuestBadgesClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ async def guest_badge_create(
+ self,
+ *,
+ email: str,
+ host_user_id: typing.Optional[str] = OMIT,
+ visit_permission: typing.Optional[bool] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[GuestBadge]:
+ """
+ Grant a Guest Badge so an email that is **not** a workspace member can
+ visit a host in Roam.
+
+ This is **not** an [On-Air event guest](https://developer.ro.am/docs/onair-api/on-air-api). It is
+ also **not** implied by [`group.add`](https://developer.ro.am/docs/api/group-add): adding an email
+ to a group does not mint a badge or send the invite. Typical onboarding is
+ `guest.badge.create` then `group.add`.
+
+ Repeating create for the same host and email returns the existing badge
+ (`visitPermission` is **not** updated) and does not re-send the invite.
+ To flip on-map access after create, use
+ [`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update).
+
+ **Access:** Organization and Personal.
+ Organization tokens require `hostUserId`. Personal tokens default to the
+ token owner; naming a different host returns `403` `access_mode_not_supported`.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only. Must not be a workspace member.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email — the same convention as
+ [`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+ Required for organization tokens. Optional for personal tokens
+ (defaults to the token owner).
+
+ visit_permission : typing.Optional[bool]
+ Whether the guest may visit the host on the map. Defaults to
+ `true`. Ignored on an idempotent retry of an existing grant
+ — use [`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update)
+ to change it.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[GuestBadge]
+ Badge created, or the existing grant returned.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "guest.badge.create",
+ method="POST",
+ json={
+ "email": email,
+ "hostUserId": host_user_id,
+ "visitPermission": visit_permission,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ GuestBadge,
+ parse_obj_as(
+ type_=GuestBadge, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ async def guest_badge_list(
+ self,
+ *,
+ email: typing.Optional[str] = None,
+ host_user_id: typing.Optional[str] = None,
+ limit: typing.Optional[int] = None,
+ cursor: typing.Optional[str] = None,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[GuestBadgeListResponse]:
+ """
+ List issued Guest Badges.
+
+ Organization tokens return every issued badge in the workspace. Personal
+ tokens return only badges the token owner issued. This is the issued
+ (host) view — the same rows `guest.badge.create` returns — not the
+ guest's hidden-inbox view.
+
+ Filter with `email` (alias-aware) and/or `hostUserId` (UUID or member
+ email). Paginate with `limit` / `cursor` (default 50, max 100). Results
+ are sorted by `(hostUserId, email)`.
+
+ Organization keys that only have `guest:write` must also request
+ `guest:read` to call list. Personal Access Tokens with `pat:guests:write`
+ already include `guest:read`.
+
+ **Access:** Organization and Personal.
+ Personal tokens naming a different host return `403`
+ `access_mode_not_supported`.
+
+ **Required scope:** `guest:read`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : typing.Optional[str]
+ Guest email. ASCII only. Matches the stored address and its verified
+ domain aliases.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email — the same convention as
+ [`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+ Archived hosts may be named (they typically have no remaining grants).
+ Personal tokens may only pass the token owner.
+
+ limit : typing.Optional[int]
+ Number of badges to return per page (default 50, max 100).
+
+ cursor : typing.Optional[str]
+ Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[GuestBadgeListResponse]
+ Issued Guest Badges for this page.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "guest.badge.list",
+ method="GET",
+ params={
+ "email": email,
+ "hostUserId": host_user_id,
+ "limit": limit,
+ "cursor": cursor,
+ },
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ GuestBadgeListResponse,
+ parse_obj_as(
+ type_=GuestBadgeListResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ async def guest_badge_update(
+ self,
+ *,
+ email: str,
+ visit_permission: bool,
+ host_user_id: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[GuestBadge]:
+ """
+ Update `visitPermission` on an existing Guest Badge.
+
+ [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) is idempotent and
+ does **not** change `visitPermission` on an existing grant. Use this
+ endpoint to flip on-map visit access after create.
+
+ If only one host in the workspace has granted this email, `hostUserId`
+ may be omitted. If several hosts have, pass `hostUserId` to pick which
+ grant to update (`400` `missing_parameter` otherwise). Same-host alias
+ rows are updated together.
+
+ Personal tokens can only update badges they issued. Naming another host
+ is `403` `access_mode_not_supported`; omitting `hostUserId` when the
+ token owner has no matching grant is `404` `not_found`.
+
+ **Access:** Organization and Personal.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only.
+
+ visit_permission : bool
+ Whether the guest may visit the host on the map.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email. Required when more than one
+ host has granted this email. Optional for a unique grant, and
+ for personal tokens (defaults to the token owner).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[GuestBadge]
+ The updated Guest Badge.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "guest.badge.update",
+ method="POST",
+ json={
+ "email": email,
+ "hostUserId": host_user_id,
+ "visitPermission": visit_permission,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ GuestBadge,
+ parse_obj_as(
+ type_=GuestBadge, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ async def guest_badge_revoke(
+ self,
+ *,
+ email: str,
+ host_user_id: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[GuestBadgeRevokeResponse]:
+ """
+ Revoke Guest Badge(s) for an email.
+
+ If only one host in the workspace has granted this email, `hostUserId` may
+ be omitted. If several hosts have, pass `hostUserId` to pick which grant
+ to revoke (`400` `missing_parameter` otherwise). Same-host alias rows are
+ all revoked together.
+
+ Returns `{ "revoked": true }` when a matching grant was found and deleted,
+ or `{ "revoked": false }` when there was nothing to revoke (already gone,
+ including after the host was archived — archiving a member deletes the
+ badges they granted). Naming an archived host does not 404.
+
+ Personal tokens can only revoke badges they issued. Naming another host is
+ `403` `access_mode_not_supported`; omitting `hostUserId` when only another
+ host granted the email is a no-op (`revoked: false`).
+
+ **Access:** Organization and Personal.
+
+ **Required scope:** `guest:write`. Personal Access Tokens use the
+ `pat:guests:write` group.
+
+ See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
+
+ Parameters
+ ----------
+ email : str
+ Guest email. ASCII only.
+
+ host_user_id : typing.Optional[str]
+ Host member. UUID or member email. Required when more than one
+ host has granted this email. Optional for a unique grant, and
+ for personal tokens (defaults to the token owner).
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[GuestBadgeRevokeResponse]
+ Revoke attempted. `revoked` is true only when a matching grant was deleted.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "guest.badge.revoke",
+ method="POST",
+ json={
+ "email": email,
+ "hostUserId": host_user_id,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ GuestBadgeRevokeResponse,
+ parse_obj_as(
+ type_=GuestBadgeRevokeResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
diff --git a/src/roamhq/guest_badges/types/__init__.py b/src/roamhq/guest_badges/types/__init__.py
new file mode 100644
index 0000000..0de8c77
--- /dev/null
+++ b/src/roamhq/guest_badges/types/__init__.py
@@ -0,0 +1,40 @@
+# This file was auto-generated by Fern from our API Definition.
+
+# isort: skip_file
+
+from __future__ import annotations
+
+import typing
+from importlib import import_module
+
+if typing.TYPE_CHECKING:
+ from .guest_badge_list_response import GuestBadgeListResponse
+ from .guest_badge_revoke_response import GuestBadgeRevokeResponse
+_dynamic_imports: typing.Dict[str, str] = {
+ "GuestBadgeListResponse": ".guest_badge_list_response",
+ "GuestBadgeRevokeResponse": ".guest_badge_revoke_response",
+}
+
+
+def __getattr__(attr_name: str) -> typing.Any:
+ module_name = _dynamic_imports.get(attr_name)
+ if module_name is None:
+ raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}")
+ try:
+ module = import_module(module_name, __package__)
+ if module_name == f".{attr_name}":
+ return module
+ else:
+ return getattr(module, attr_name)
+ except ImportError as e:
+ raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e
+ except AttributeError as e:
+ raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e
+
+
+def __dir__():
+ lazy_attrs = list(_dynamic_imports.keys())
+ return sorted(lazy_attrs)
+
+
+__all__ = ["GuestBadgeListResponse", "GuestBadgeRevokeResponse"]
diff --git a/src/roamhq/guest_badges/types/guest_badge_list_response.py b/src/roamhq/guest_badges/types/guest_badge_list_response.py
new file mode 100644
index 0000000..2557ebd
--- /dev/null
+++ b/src/roamhq/guest_badges/types/guest_badge_list_response.py
@@ -0,0 +1,36 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from ...core.serialization import FieldMetadata
+from ...types.guest_badge import GuestBadge
+
+
+class GuestBadgeListResponse(UniversalBaseModel):
+ guest_badges: typing_extensions.Annotated[
+ typing.List[GuestBadge], FieldMetadata(alias="guestBadges"), pydantic.Field(alias="guestBadges")
+ ]
+ next_cursor: typing_extensions.Annotated[
+ typing.Optional[str],
+ FieldMetadata(alias="nextCursor"),
+ pydantic.Field(
+ alias="nextCursor", description="Opaque cursor for the next page. Omitted when there are no more results."
+ ),
+ ] = None
+ """
+ Opaque cursor for the next page. Omitted when there are no more results.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/roamhq/guest_badges/types/guest_badge_revoke_response.py b/src/roamhq/guest_badges/types/guest_badge_revoke_response.py
new file mode 100644
index 0000000..94252ed
--- /dev/null
+++ b/src/roamhq/guest_badges/types/guest_badge_revoke_response.py
@@ -0,0 +1,24 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+
+
+class GuestBadgeRevokeResponse(UniversalBaseModel):
+ revoked: bool = pydantic.Field()
+ """
+ Whether a matching grant was deleted.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/roamhq/reference.md b/src/roamhq/reference.md
index 95cc1d2..c1db66e 100644
--- a/src/roamhq/reference.md
+++ b/src/roamhq/reference.md
@@ -3012,7 +3012,7 @@ when the token has `user:read.email`. Cannot be combined with `ids`.
-
-**expand:** `typing.Optional[str]` — Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set.
+**expand:** `typing.Optional[str]` — Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set. Write that field with `user.status.set` / `.clear`.
@@ -3117,7 +3117,7 @@ client.user.info()
-
-**expand:** `typing.Optional[str]` — 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.
+**expand:** `typing.Optional[str]` — 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. Write that field with `user.status.set` / `.clear`.
@@ -3138,7 +3138,7 @@ client.user.info()
## Users
-client.users.user_activity_set(...) -> UserActivity
+client.users.user_status_set(...) -> UserStatusSetResponse
-
@@ -3150,30 +3150,39 @@ client.user.info()
-
-Paint a badge (and optional glow) on a user's seat for work happening
-outside Roam — a phone call, a browser meeting, a CRM session. Pass
-`dnd: true` to also put their assigned office in Do Not Disturb.
+Record an absence on a workspace member — the same Will Return Today /
+Out of Roam field the desktop client writes, already readable via
+[`user.info?expand=status`](https://developer.ro.am/docs/api/user-info) and
+[`user.status.update`](https://developer.ro.am/docs/webhooks/user-status-update).
-The integration owns the lifecycle: `set` when the session starts,
-`clear` when it ends. Re-posting the same `externalId` is the heartbeat
-for long-running sessions — it refreshes `expiresAt` and, unless you
-send `startedAt`, keeps the original start time. Roam stamps expiry
-itself (default 10 minutes, maximum 60) so a dropped "ended" webhook
-cannot leave a permanent glow.
+This is **not** [external activity](https://developer.ro.am/docs/guides/user-activity). Use
+`user.status.set` for HR absences (sick leave, vacation, parental leave,
+public holidays). Use `user.activity.set` for a short-lived on-map glow
+/ emoji (phone call, browser meeting).
-`externalId` is unique per (integration, user). Two apps can hold
-activities on the same person at once; you can only update or clear
-your own rows.
+`willReturn` is last-writer-wins with the desktop client. Setting it
+does **not** check the user out, does **not** enable Do Not Disturb, and
+does **not** accept a `status` enum (`checkedIn` / `checkedOut` stay
+read-only).
-See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
-TTL, stacking, and where the indicator appears on the map.
+`outOfRoam` defaults to `true` (persistent Out of Roam, up to 2 years).
+Pass `outOfRoam: false` for same-day Will Return Today (`returnTime`
+must be less than 10 hours from now).
+
+Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ASCII email (same convention as `group.create` members). Third-party
+systems that only have an email do not need a UUID lookup first.
+
+See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for the two
+modes, persistence across check-in, and an HRIS example.
**Access:** Organization and Personal. Organization tokens may target
-any user in the workspace. Personal tokens (OAuth or PAT) may target
-only the token owner.
+any active member in the workspace. Personal tokens (OAuth or PAT) may
+target only the token owner.
-**Required scope:** `user:write.activity`. Personal Access Tokens skip
+**Required scope:** `user:write.status`. Personal Access Tokens skip
this check; personal-mode OAuth installs must still request the scope.
+Reading the field back via `user.info` still needs `user:read.status`.
@@ -3188,25 +3197,23 @@ this check; personal-mode OAuth installs must still request the scope.
-
```python
-from roamhq import RoamClient, UserActivityDisplay
+from roamhq import RoamClient
from roamhq.environment import RoamClientEnvironment
+from roamhq.users import UserStatusSetRequestWillReturn
+import datetime
client = RoamClient(
token="",
environment=RoamClientEnvironment.DEFAULT,
)
-client.users.user_activity_set(
- user_id="0cc74785-e31e-4403-aa5e-0cc7c1897e66",
- external_id="justcall:call:CA123",
- display=UserActivityDisplay(
- emoji="📞",
- title="On a customer call",
- subtitle="JustCall · Acme Corp",
- color="green",
+client.users.user_status_set(
+ user_id="ada@example.com",
+ will_return=UserStatusSetRequestWillReturn(
+ return_time=datetime.datetime.fromisoformat("2026-09-22T09:00:00+00:00"),
+ reason="On vacation",
+ out_of_roam=True,
),
- ttl_seconds=1800,
- dnd=True,
)
```
@@ -3225,53 +3232,11 @@ client.users.user_activity_set(
**user_id:** `str`
-Target user. Bare or tagged UUID. Personal tokens may only
-pass their own user.
-
-
-
-
-
--
-
-**external_id:** `str`
-
-Caller-chosen session id, unique per integration and user.
-Re-using it upserts the existing row (heartbeat). At most
-128 Unicode code points.
-
-
-
-
-
--
-
-**display:** `UserActivityDisplay`
-
-
-
-
-
--
-
-**ttl_seconds:** `typing.Optional[int]`
-
-Seconds from now until expiry. Mutually exclusive with
-`expiresAt`. Values above 3600 are **clamped** to 60
-minutes, not rejected. Default when both are omitted: 600
-(10 minutes).
-
-
-
-
-
--
-
-**expires_at:** `typing.Optional[datetime.datetime]`
-
-Absolute expiry (RFC3339, must be in the future). Mutually
-exclusive with `ttlSeconds`. Instants more than 60 minutes
-ahead are clamped to that maximum.
+Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+(same convention as `group.create` members). Personal
+tokens may only pass their own user. Does not require
+`user:read.email` — email is an identifier, not a
+disclosure.
@@ -3279,12 +3244,10 @@ ahead are clamped to that maximum.
-
-**started_at:** `typing.Optional[datetime.datetime]`
+**will_return:** `UserStatusSetRequestWillReturn`
-Optional session start (RFC3339). Omit on heartbeats to
-preserve the original. A future value is clamped to the
-server's now (clock skew; also so one integration cannot
-pin the newest-first projection slot).
+Absence to write. Required. Replaces any existing Will
+Return / Out of Roam on this user.
@@ -3292,13 +3255,11 @@ pin the newest-first projection slot).
-
-**dnd:** `typing.Optional[bool]`
+**status:** `typing.Optional[str]`
-If true, this activity contributes Do Not Disturb on the
-user's **own assigned office** until it is cleared or
-expires. Defaults to false — a badge does not lock an
-office unless you opt in. Stacks with Zoom/Meet auto-DND
-and other integrations' DND-flagged rows.
+Rejected. Check-in status is read-only; absences go in
+`willReturn`. Sending this field returns `400`
+`invalid_arguments`.
@@ -3318,7 +3279,7 @@ and other integrations' DND-flagged rows.
-client.users.user_activity_clear(...)
+client.users.user_status_clear(...)
-
@@ -3330,22 +3291,21 @@ and other integrations' DND-flagged rows.
-
-End an activity previously created with [`user.activity.set`](https://developer.ro.am/docs/api/user-activity-set).
-The row is keyed by this integration plus `userId` and `externalId` —
-you cannot clear another app's activity.
+Remove the Will Return / Out of Roam previously written with
+[`user.status.set`](https://developer.ro.am/docs/api/user-status-set) or the desktop client.
+Clears both same-day Will Return Today and persistent Out of Roam.
-Clearing a missing, already-cleared, or already-expired `externalId`
-still returns **204**. Integrations retry "session ended" webhooks, and
-the row may have expired in the meantime.
+Clearing when nothing is set still returns **204**. This call does
+**not** change check-in status.
-See [External activity](https://developer.ro.am/docs/guides/user-activity) for TTL, DND
-stacking, and what happens on the map when the last activity clears.
+See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for
+persistence, check-in interaction, and the HRIS lifecycle.
**Access:** Organization and Personal. Organization tokens may target
-any user in the workspace. Personal tokens (OAuth or PAT) may target
-only the token owner.
+any active member in the workspace. Personal tokens (OAuth or PAT) may
+target only the token owner.
-**Required scope:** `user:write.activity`. Personal Access Tokens skip
+**Required scope:** `user:write.status`. Personal Access Tokens skip
this check; personal-mode OAuth installs must still request the scope.
@@ -3369,9 +3329,8 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.users.user_activity_clear(
- user_id="0cc74785-e31e-4403-aa5e-0cc7c1897e66",
- external_id="justcall:call:CA123",
+client.users.user_status_clear(
+ user_id="ada@example.com",
)
```
@@ -3390,16 +3349,9 @@ client.users.user_activity_clear(
**user_id:** `str`
-Target user. Bare or tagged UUID. Personal tokens may only
-pass their own user.
-
-
-
-
-
--
-
-**external_id:** `str` — The `externalId` previously passed to `user.activity.set`.
+Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+(same convention as `group.create` members). Personal
+tokens may only pass their own user.
@@ -3419,7 +3371,7 @@ pass their own user.
-client.users.user_activity_list(...) -> UserActivityListResponse
+client.users.user_status_bubble_set(...) -> UserStatusBubbleResponse
-
@@ -3431,24 +3383,13 @@ pass their own user.
-
-Return every **currently live** external activity for a user — every
-integration's rows, not only yours. Expired rows are omitted even
-before the server reaper runs. Not paginated; ordered newest
-`startedAt` first.
-
-The map may show fewer entries than this list (the client projection
-keeps the top three, always including at least one DND-flagged row).
-`.list` is the source of truth for what is still live.
+Replaces the shared UI/API thought bubble. Text is trimmed and limited to 1–20 Unicode code points. Omitted or null ttlSeconds defaults to 86400; integers from 300 through 86400 are accepted. Durations below 5 minutes or above 24 hours are rejected. Automatic map removal can take up to about a minute after expiration. Repeating set refreshes expiration. Sending expiresAt is rejected.
-See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
-TTL, and where indicators appear.
+See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
-**Access:** Organization and Personal. Organization tokens may list
-any user in the workspace. Personal tokens (OAuth or PAT) may list
-only the token owner.
+**Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
-**Required scope:** `user:read.activity`. Personal Access Tokens skip
-this check; personal-mode OAuth installs must still request the scope.
+**Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
@@ -3471,8 +3412,10 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.users.user_activity_list(
- user_id="userId",
+client.users.user_status_bubble_set(
+ user_id="ada@example.com",
+ text="At lunch 🍎",
+ ttl_seconds=3600,
)
```
@@ -3489,10 +3432,23 @@ client.users.user_activity_list(
-
-**user_id:** `str`
+**user_id:** `str` — Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+
+
+
+
+-
+
+**text:** `str` — Text to trim and store. Must contain 1–20 Unicode code points after trimming. Blank text is rejected.
+
+
+
+
+
+-
-Target user. Bare or tagged UUID. Personal tokens may only pass
-their own user.
+**ttl_seconds:** `typing.Optional[int]` — Optional duration. Omit or pass null for 24 hours. Durations outside 300–86400 seconds are rejected.
@@ -3512,7 +3468,7 @@ their own user.
-client.users.messageevent_export(...) -> str
+client.users.user_status_bubble_get(...) -> UserStatusBubbleResponse
-
@@ -3524,80 +3480,13 @@ their own user.
-
-Obtain a daily message event export containing DMs and group
-chats within your account.
-
-For customers with archival enabled (please reach out to a Roam
-ArchiTech to get this process started), at the end of every day,
-we export all message events for a particular day as a JSON Lines file.
-This file contains all messages sent:
-- by a Roam user who is a member of your organization
-- into a chat containing (at the time of export) at least one Roam user who is a member of your organization
-- by a bot integration that is part of your organization
-
-This file also contains message edit and deletion events that meet the above criteria.
-We specifically exclude waves, room invitations, and other non-message content
-(that may appear as chats within the Roam application) from the export.
-
-**Access:** Organization only.
-
-**Required scope:** `admin:compliance:read`
-
-### Message Event Structure
-
-Each line within the file is a JSON object containing the following fields:
-- eventType: a string that is one of “sent”, “edited”, or “deleted”
-- chatId: a UUIDv4 identifier for a particular chat. All messages within the same chat shared the same chatId.
-- 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.
-- timestamp: the Unix epoch timestamp when the message was originally sent in numerical format.
-- messageId: an internal UUIDv4 identifier as a string
-- sender: a “Participant” object that identifiers the message sender
-- contentType: a string that is one of the contentTypes associated with the “MessageContent” object
-- content: a “MessageContent” object that contains the message’s content
-
-### Participant
-
-A Participant is a JSON object that contains three common fields: “participantType”, “id”, and “displayName”
-- participantType: one of “email”, “bot”, or “occupant”
-- id: a UUID identifier for the participant
-- displayName: the name associated with the account or an empty string if not provided
-
-Depending on the participant type, the object also contains additional fields:
-
-Email Participant (a human user with a Roam user account)
-- email: the email of the participant
-
-Bot Participant (an automated user maintained by the Roam team or created via the Roam API)
-- roamId: the roam ID associated with the integration
-- integrationId: a unique integration ID name provided by the bot creator
-- botCode: a unique identifier
-
-### Message Content
-
-A “MessageContent” object is a JSON object that contains the field “contentType” and,
-depending on the content type, contains additional fields:
-
-*Text Content* (contentType = “text”)
-- text: the text in plaintext
-- markdownText: the text in Markdown format
-- attachments: A list of attachment objects
-
-*Emoji Content* (contentType = “emoji”)
-- text: text representation of the emoji
-- colons: emoji in :emoji: format
-- fileUrl: an optional field containing the URL to a custom emoji image
+Returns the shared UI/API thought bubble, or null when no live bubble exists. Expired bubbles are omitted before storage cleanup runs.
-*Item Content* (contentType = “item”)
-- itemUrl: the URL where the file can be downloaded from
-- itemType: the type of item (e.g. "photo", "pdf", "blob", "video", "audio", etc.)
+See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
-*Text Snippet Content* (contentType = "textSnippet")
-- text: the content of the snippet
-- language: the language of the snippet
+**Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
-*Members Changed Content* (contentType = “membersChanged”)
-- added: a list of Participant objects corresponding to all participants added in this event
-- removed: a list of Participant objects corresponding to all participants removed in this event
+**Required scope:** `user:read.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
@@ -3620,8 +3509,8 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.users.messageevent_export(
- date="2026-01-21",
+client.users.user_status_bubble_get(
+ user_id="userId",
)
```
@@ -3638,7 +3527,7 @@ client.users.messageevent_export(
-
-**date:** `str` — The UTC date to fetch the export for in YYYY-MM-DD format.
+**user_id:** `str` — Bare UUID, tagged U-… ID, or ASCII email of the target user.
@@ -3658,8 +3547,7 @@ client.users.messageevent_export(
-## UserAuditLog
-client.user_audit_log.list(...) -> ListUserAuditLogResponse
+client.users.user_status_bubble_clear(...)
-
@@ -3671,9 +3559,13 @@ client.users.messageevent_export(
-
-Get a list of user audit log entries for the account.
+Clears the shared thought bubble, including a bubble written by the UI or another integration. Repeating clear is a successful no-op. This does not clear Will Return or external activities.
-**Required scope:** `userauditlog:read`
+See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+**Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+**Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
@@ -3696,7 +3588,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.user_audit_log.list()
+client.users.user_status_bubble_clear(
+ user_id="ada@example.com",
+)
```
@@ -3712,7 +3606,7 @@ client.user_audit_log.list()
-
-**date:** `typing.Optional[str]` — The date to pull audit log entries from. All activities from that date in UTC are returned.
+**user_id:** `str` — Bare UUID, tagged U-… ID, or ASCII email of the target user.
@@ -3732,8 +3626,7 @@ client.user_audit_log.list()
-## Conversation
-client.conversation.list(...) -> ListConversationResponse
+client.users.user_activity_set(...) -> UserActivity
-
@@ -3745,23 +3638,948 @@ client.user_audit_log.list()
-
-Lists conversations (meetings) that occurred in your Roam, with participant details.
+Paint a badge (and optional glow) on a user's seat for work happening
+outside Roam — a phone call, a browser meeting, a CRM session. Pass
+`dnd: true` to also put their assigned office in Do Not Disturb.
-**Access:**
-- **Organization with [`admin:meetings:read`](https://developer.ro.am/docs/guides/scopes#meeting-width-adminmeetingsread)**
- (or a grandfathered roam-wide API key): all conversations in the workspace.
-- **Personal access tokens:** supported — returns only conversations the
- token owner participated in (matched by confirmed email).
-- **Organization without roam-wide meeting access** must use
- [`/meeting.list`](https://developer.ro.am/docs/api/meeting-list) instead (`403`).
+The integration owns the lifecycle: `set` when the session starts,
+`clear` when it ends. Re-posting the same `externalId` is the heartbeat
+for long-running sessions — it refreshes `expiresAt` and, unless you
+send `startedAt`, keeps the original start time. Roam stamps expiry
+itself (default 10 minutes, maximum 60) so a dropped "ended" webhook
+cannot leave a permanent glow.
-**Required scope:** `meetings:read` (add `admin:meetings:read` for roam-wide org access)
+`externalId` is unique per (integration, user). Two apps can hold
+activities on the same person at once; you can only update or clear
+your own rows.
-Participant details require `user:read` scope. Email addresses require `user:read.email` scope.
-
-
-
-
+See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
+TTL, stacking, and where the indicator appears on the map.
+
+Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ASCII email (same convention as `group.create` members). Third-party
+systems that only have an email do not need a UUID lookup first.
+
+**Access:** Organization and Personal. Organization tokens may target
+any user in the workspace. Personal tokens (OAuth or PAT) may target
+only the token owner.
+
+**Required scope:** `user:write.activity`. Personal Access Tokens skip
+this check; personal-mode OAuth installs must still request the scope.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient, UserActivityDisplay
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.users.user_activity_set(
+ user_id="ada@example.com",
+ external_id="justcall:call:CA123",
+ display=UserActivityDisplay(
+ emoji="📞",
+ title="On a customer call",
+ subtitle="JustCall · Acme Corp",
+ color="green",
+ ),
+ ttl_seconds=1800,
+ dnd=True,
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**user_id:** `str`
+
+Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+(same convention as `group.create` members). Personal
+tokens may only pass their own user. Does not require
+`user:read.email` — email is an identifier, not a
+disclosure.
+
+
+
+
+
+-
+
+**external_id:** `str`
+
+Caller-chosen session id, unique per integration and user.
+Re-using it upserts the existing row (heartbeat). At most
+128 Unicode code points.
+
+
+
+
+
+-
+
+**display:** `UserActivityDisplay`
+
+
+
+
+
+-
+
+**ttl_seconds:** `typing.Optional[int]`
+
+Seconds from now until expiry. Mutually exclusive with
+`expiresAt`. Values above 3600 are **clamped** to 60
+minutes, not rejected. Default when both are omitted: 600
+(10 minutes).
+
+
+
+
+
+-
+
+**expires_at:** `typing.Optional[datetime.datetime]`
+
+Absolute expiry (RFC3339, must be in the future). Mutually
+exclusive with `ttlSeconds`. Instants more than 60 minutes
+ahead are clamped to that maximum.
+
+
+
+
+
+-
+
+**started_at:** `typing.Optional[datetime.datetime]`
+
+Optional session start (RFC3339). Omit on heartbeats to
+preserve the original. A future value is clamped to the
+server's now (clock skew; also so one integration cannot
+pin the newest-first projection slot).
+
+
+
+
+
+-
+
+**dnd:** `typing.Optional[bool]`
+
+If true, this activity contributes Do Not Disturb on the
+user's **own assigned office** until it is cleared or
+expires. Defaults to false — a badge does not lock an
+office unless you opt in. Stacks with Zoom/Meet auto-DND
+and other integrations' DND-flagged rows.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.users.user_activity_clear(...)
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+End an activity previously created with [`user.activity.set`](https://developer.ro.am/docs/api/user-activity-set).
+The row is keyed by this integration plus `userId` and `externalId` —
+you cannot clear another app's activity.
+
+Clearing a missing, already-cleared, or already-expired `externalId`
+still returns **204**. Integrations retry "session ended" webhooks, and
+the row may have expired in the meantime.
+
+See [External activity](https://developer.ro.am/docs/guides/user-activity) for TTL, DND
+stacking, and what happens on the map when the last activity clears.
+
+**Access:** Organization and Personal. Organization tokens may target
+any user in the workspace. Personal tokens (OAuth or PAT) may target
+only the token owner.
+
+**Required scope:** `user:write.activity`. Personal Access Tokens skip
+this check; personal-mode OAuth installs must still request the scope.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.users.user_activity_clear(
+ user_id="ada@example.com",
+ external_id="justcall:call:CA123",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**user_id:** `str`
+
+Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+(same convention as `group.create` members). Personal
+tokens may only pass their own user.
+
+
+
+
+
+-
+
+**external_id:** `str` — The `externalId` previously passed to `user.activity.set`.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.users.user_activity_list(...) -> UserActivityListResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Return every **currently live** external activity for a user — every
+integration's rows, not only yours. Expired rows are omitted even
+before the server reaper runs. Not paginated; ordered newest
+`startedAt` first.
+
+The map may show fewer entries than this list (the client projection
+keeps the top three, always including at least one DND-flagged row).
+`.list` is the source of truth for what is still live.
+
+See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
+TTL, and where indicators appear.
+
+**Access:** Organization and Personal. Organization tokens may list
+any user in the workspace. Personal tokens (OAuth or PAT) may list
+only the token owner.
+
+**Required scope:** `user:read.activity`. Personal Access Tokens skip
+this check; personal-mode OAuth installs must still request the scope.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.users.user_activity_list(
+ user_id="userId",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**user_id:** `str`
+
+Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+(same convention as `group.create` members). Personal tokens
+may only pass their own user.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.users.messageevent_export(...) -> str
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Obtain a daily message event export containing DMs and group
+chats within your account.
+
+For customers with archival enabled (please reach out to a Roam
+ArchiTech to get this process started), at the end of every day,
+we export all message events for a particular day as a JSON Lines file.
+This file contains all messages sent:
+- by a Roam user who is a member of your organization
+- into a chat containing (at the time of export) at least one Roam user who is a member of your organization
+- by a bot integration that is part of your organization
+
+This file also contains message edit and deletion events that meet the above criteria.
+We specifically exclude waves, room invitations, and other non-message content
+(that may appear as chats within the Roam application) from the export.
+
+**Access:** Organization only.
+
+**Required scope:** `admin:compliance:read`
+
+### Message Event Structure
+
+Each line within the file is a JSON object containing the following fields:
+- eventType: a string that is one of “sent”, “edited”, or “deleted”
+- chatId: a UUIDv4 identifier for a particular chat. All messages within the same chat shared the same chatId.
+- 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.
+- timestamp: the Unix epoch timestamp when the message was originally sent in numerical format.
+- messageId: an internal UUIDv4 identifier as a string
+- sender: a “Participant” object that identifiers the message sender
+- contentType: a string that is one of the contentTypes associated with the “MessageContent” object
+- content: a “MessageContent” object that contains the message’s content
+
+### Participant
+
+A Participant is a JSON object that contains three common fields: “participantType”, “id”, and “displayName”
+- participantType: one of “email”, “bot”, or “occupant”
+- id: a UUID identifier for the participant
+- displayName: the name associated with the account or an empty string if not provided
+
+Depending on the participant type, the object also contains additional fields:
+
+Email Participant (a human user with a Roam user account)
+- email: the email of the participant
+
+Bot Participant (an automated user maintained by the Roam team or created via the Roam API)
+- roamId: the roam ID associated with the integration
+- integrationId: a unique integration ID name provided by the bot creator
+- botCode: a unique identifier
+
+### Message Content
+
+A “MessageContent” object is a JSON object that contains the field “contentType” and,
+depending on the content type, contains additional fields:
+
+*Text Content* (contentType = “text”)
+- text: the text in plaintext
+- markdownText: the text in Markdown format
+- attachments: A list of attachment objects
+
+*Emoji Content* (contentType = “emoji”)
+- text: text representation of the emoji
+- colons: emoji in :emoji: format
+- fileUrl: an optional field containing the URL to a custom emoji image
+
+*Item Content* (contentType = “item”)
+- itemUrl: the URL where the file can be downloaded from
+- itemType: the type of item (e.g. "photo", "pdf", "blob", "video", "audio", etc.)
+
+*Text Snippet Content* (contentType = "textSnippet")
+- text: the content of the snippet
+- language: the language of the snippet
+
+*Members Changed Content* (contentType = “membersChanged”)
+- added: a list of Participant objects corresponding to all participants added in this event
+- removed: a list of Participant objects corresponding to all participants removed in this event
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.users.messageevent_export(
+ date="2026-01-21",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**date:** `str` — The UTC date to fetch the export for in YYYY-MM-DD format.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+## UserAuditLog
+client.user_audit_log.list(...) -> ListUserAuditLogResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Get a list of user audit log entries for the account.
+
+**Required scope:** `userauditlog:read`
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.user_audit_log.list()
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**date:** `typing.Optional[str]` — The date to pull audit log entries from. All activities from that date in UTC are returned.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+## Conversation
+client.conversation.list(...) -> ListConversationResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Lists conversations (meetings) that occurred in your Roam, with participant details.
+
+**Access:**
+- **Organization with [`admin:meetings:read`](https://developer.ro.am/docs/guides/scopes#meeting-width-adminmeetingsread)**
+ (or a grandfathered roam-wide API key): all conversations in the workspace.
+- **Personal access tokens:** supported — returns only conversations the
+ token owner participated in (matched by confirmed email).
+- **Organization without roam-wide meeting access** must use
+ [`/meeting.list`](https://developer.ro.am/docs/api/meeting-list) instead (`403`).
+
+**Required scope:** `meetings:read` (add `admin:meetings:read` for roam-wide org access)
+
+Participant details require `user:read` scope. Email addresses require `user:read.email` scope.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.conversation.list()
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**before:** `typing.Optional[datetime.datetime]` — Only return conversations that started before this ISO-8601 timestamp.
+
+
+
+
+
+-
+
+**after:** `typing.Optional[datetime.datetime]` — Only return conversations that started after this ISO-8601 timestamp.
+
+
+
+
+
+-
+
+**ascending:** `typing.Optional[bool]` — Sort results in ascending order by start time. Default is descending (newest first).
+
+
+
+
+
+-
+
+**limit:** `typing.Optional[int]` — The number of conversations to return per response.
+
+
+
+
+
+-
+
+**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+## Meeting
+client.meeting.list(...) -> ListMeetingResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+List meetings, ordered newest-first.
+
+**Access:** Organization and Personal. Personal tokens return meetings the
+authenticated user participated in. Organization tokens return every meeting
+in the Roam only with [`admin:meetings:read`](https://developer.ro.am/docs/guides/scopes#meeting-width-adminmeetingsread);
+without it, results are limited to meetings the install's bot has access to.
+
+**Required scope:** `meetings:read` (add `admin:meetings:read` for roam-wide org access)
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.meeting.list()
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**before:** `typing.Optional[datetime.datetime]` — Only return meetings that started before this time (RFC-3339). Sub-millisecond precision is truncated.
+
+
+
+
+
+-
+
+**after:** `typing.Optional[datetime.datetime]` — Only return meetings that started after this time (RFC-3339). Sub-millisecond precision is truncated.
+
+
+
+
+
+-
+
+**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+
+
+
+
+
+-
+
+**limit:** `typing.Optional[int]`
+
+Number of meetings to return per page. Capped to **10** when
+`expand` includes `summary`, `actionItems`, or `chapters`, since
+expanded payloads are substantially larger.
+
+
+
+
+
+-
+
+**expand:** `typing.Optional[str]`
+
+Comma-separated list of fields to inline on each meeting. Allowed
+values are `summary`, `actionItems`, and `chapters` — same shape
+as on [`/meeting.info`](https://developer.ro.am/docs/api/meeting-info). Use this to
+avoid N+1 follow-up calls when scanning many recent meetings.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.meeting.info(...) -> InfoMeetingResponse
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Get detailed information about a specific meeting, including AI-generated summary, action items, and chapters.
+
+Participants are included inline up to the `maxParticipants` limit. For meetings with more participants, use [`/meeting.participants`](https://developer.ro.am/docs/api/meeting-participants) to paginate through the full list.
+
+**Access:** Organization and Personal. Personal tokens are limited to meetings
+the authenticated user participated in. Organization tokens without
+[`admin:meetings:read`](https://developer.ro.am/docs/guides/scopes#meeting-width-adminmeetingsread)
+are limited to meetings the install's bot has access to.
+
+**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)
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.meeting.info(
+ id="id",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**id:** `str` — The meeting ID.
+
+
+
+
+
+-
+
+**max_participants:** `typing.Optional[int]` — Maximum number of participants to resolve and include inline. Use `/meeting.participants` for full pagination.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.meeting.participants(...) -> ParticipantsMeetingResponse
+
+-
+
+#### 📝 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`](https://developer.ro.am/docs/api/meeting-info).
+
+Pagination uses an **opaque cursor** (not a row offset). Pass `nextCursor`
+from a previous response as `cursor` to fetch the next page. Invalid cursors
+return `error: "invalid_cursor"` — see [Responses and Errors](https://developer.ro.am/docs/guides/responses-and-errors).
+
+**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.
+
+**Required scope:** `meetings:read` and `user:read` (add `user:read.email` for participant emails)
+
+
+
+
#### 🔌 Usage
@@ -3780,7 +4598,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.conversation.list()
+client.meeting.participants(
+ id="id",
+)
```
@@ -3796,7 +4616,7 @@ client.conversation.list()
-
-**before:** `typing.Optional[datetime.datetime]` — Only return conversations that started before this ISO-8601 timestamp.
+**id:** `str` — The meeting ID.
@@ -3804,7 +4624,7 @@ client.conversation.list()
-
-**after:** `typing.Optional[datetime.datetime]` — Only return conversations that started after this ISO-8601 timestamp.
+**limit:** `typing.Optional[int]` — Number of participants to return per page (default 50, max 200).
@@ -3812,7 +4632,7 @@ client.conversation.list()
-
-**ascending:** `typing.Optional[bool]` — Sort results in ascending order by start time. Default is descending (newest first).
+**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not parse or construct cursors yourself.
@@ -3820,15 +4640,89 @@ client.conversation.list()
-
-**limit:** `typing.Optional[int]` — The number of conversations to return per response.
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+client.meeting.transcript(...) -> TranscriptMeetingResponse
-
-**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+#### 📝 Description
+
+
+-
+
+
+-
+
+Retrieve the transcript for a meeting.
+
+Supports content negotiation:
+- **JSON** (default): Returns structured transcript with cues containing speaker IDs, text, and timing
+- **WebVTT**: Set `Accept: text/vtt` header to receive standard WebVTT format with speaker voice tags
+
+**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.
+
+**Required scope:** `meetings:read`
+
+**Errors** (see [Responses and Errors](https://developer.ro.am/docs/guides/responses-and-errors)):
+
+| `error` code | Meaning |
+|--------------|---------|
+| `meeting_not_found` | Unknown or inaccessible meeting |
+| `transcript_pending` | Not ready yet — retry later (may include `Retry-After`) |
+| `transcript_unavailable` | Meeting was not transcribed — stop retrying |
+| `upstream_timeout` | Timed out waiting on an upstream service — retry |
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.meeting.transcript(
+ id="id",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**id:** `str` — The meeting ID.
@@ -3848,8 +4742,7 @@ client.conversation.list()
-## Meeting
-client.meeting.list(...) -> ListMeetingResponse
+client.meeting.search(...) -> SearchMeetingResponse
-
@@ -3861,14 +4754,11 @@ client.conversation.list()
-
-List meetings, ordered newest-first.
+AI-powered search across meeting transcripts and summaries.
-**Access:** Organization and Personal. Personal tokens return meetings the
-authenticated user participated in. Organization tokens return every meeting
-in the Roam only with [`admin:meetings:read`](https://developer.ro.am/docs/guides/scopes#meeting-width-adminmeetingsread);
-without it, results are limited to meetings the install's bot has access to.
+**Access:** Personal access only. Organization (account-level) tokens are not supported.
-**Required scope:** `meetings:read` (add `admin:meetings:read` for roam-wide org access)
+**Required scope:** `meetings:read`
@@ -3891,7 +4781,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.meeting.list()
+client.meeting.search(
+ query="query",
+)
```
@@ -3907,7 +4799,7 @@ client.meeting.list()
-
-**before:** `typing.Optional[datetime.datetime]` — Only return meetings that started before this time (RFC-3339). Sub-millisecond precision is truncated.
+**query:** `str` — Search query string.
@@ -3915,7 +4807,7 @@ client.meeting.list()
-
-**after:** `typing.Optional[datetime.datetime]` — Only return meetings that started after this time (RFC-3339). Sub-millisecond precision is truncated.
+**after:** `typing.Optional[datetime.date]` — Only return results from meetings after this date (YYYY-MM-DD).
@@ -3923,7 +4815,7 @@ client.meeting.list()
-
-**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+**before:** `typing.Optional[datetime.date]` — Only return results from meetings before this date (YYYY-MM-DD).
@@ -3931,24 +4823,93 @@ client.meeting.list()
-
-**limit:** `typing.Optional[int]`
+**timezone:** `typing.Optional[str]` — Timezone for date interpretation (e.g. "America/New_York").
+
+
+
-Number of meetings to return per page. Capped to **10** when
-`expand` includes `summary`, `actionItems`, or `chapters`, since
-expanded payloads are substantially larger.
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+client.meeting.prompt(...) -> PromptMeetingResponse
-
-**expand:** `typing.Optional[str]`
+#### 📝 Description
-Comma-separated list of fields to inline on each meeting. Allowed
-values are `summary`, `actionItems`, and `chapters` — same shape
-as on [`/meeting.info`](https://developer.ro.am/docs/api/meeting-info). Use this to
-avoid N+1 follow-up calls when scanning many recent meetings.
+
+-
+
+
+-
+
+Ask an AI question about a meeting's transcript content. Returns a natural language response based on the meeting transcript.
+
+**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.
+
+**Required scope:** `meetings:read`
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
+
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
+
+client.meeting.prompt(
+ id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ prompt="What action items were assigned to Alex?",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**id:** `str` — The meeting ID.
+
+
+
+
+
+-
+
+**prompt:** `str` — The question to ask about the meeting.
@@ -3968,7 +4929,7 @@ avoid N+1 follow-up calls when scanning many recent meetings.
-client.meeting.info(...) -> InfoMeetingResponse
+client.meeting.share_link(...) -> ShareLinkMeetingResponse
-
@@ -3980,16 +4941,15 @@ avoid N+1 follow-up calls when scanning many recent meetings.
-
-Get detailed information about a specific meeting, including AI-generated summary, action items, and chapters.
+Returns a shareable URL for a meeting that you can distribute to others. Pass the `id` of a meeting obtained from [`/meeting.list`](https://developer.ro.am/docs/api/meeting-list) or [`/meeting.info`](https://developer.ro.am/docs/api/meeting-info).
-Participants are included inline up to the `maxParticipants` limit. For meetings with more participants, use [`/meeting.participants`](https://developer.ro.am/docs/api/meeting-participants) to paginate through the full list.
+This 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.
-**Access:** Organization and Personal. Personal tokens are limited to meetings
-the authenticated user participated in. Organization tokens without
-[`admin:meetings:read`](https://developer.ro.am/docs/guides/scopes#meeting-width-adminmeetingsread)
-are limited to meetings the install's bot has access to.
+Creating 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.
-**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)
+**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.
+
+**Required scope:** `meetings:read`
@@ -4012,8 +4972,8 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.meeting.info(
- id="id",
+client.meeting.share_link(
+ id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
)
```
@@ -4038,14 +4998,6 @@ client.meeting.info(
-
-**max_participants:** `typing.Optional[int]` — Maximum number of participants to resolve and include inline. Use `/meeting.participants` for full pagination.
-
-
-
-
-
--
-
**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -4058,7 +5010,7 @@ client.meeting.info(
-client.meeting.participants(...) -> ParticipantsMeetingResponse
+client.meeting.create_link(...) -> CreateLinkMeetingResponse
-
@@ -4070,15 +5022,11 @@ client.meeting.info(
-
-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`](https://developer.ro.am/docs/api/meeting-info).
-
-Pagination uses an **opaque cursor** (not a row offset). Pass `nextCursor`
-from a previous response as `cursor` to fetch the next page. Invalid cursors
-return `error: "invalid_cursor"` — see [Responses and Errors](https://developer.ro.am/docs/guides/responses-and-errors).
+Create a meeting link.
-**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.
+**Access:** Organization and Personal. In Organization mode, specify the host by email. In Personal mode, the host defaults to the authenticated user.
-**Required scope:** `meetings:read` and `user:read` (add `user:read.email` for participant emails)
+**Required scope:** `meeting:write` or `meetinglink:write`
@@ -4095,31 +5043,55 @@ return `error: "invalid_cursor"` — see [Responses and Errors](https://develope
```python
from roamhq import RoamClient
from roamhq.environment import RoamClientEnvironment
+import datetime
client = RoamClient(
token="",
environment=RoamClientEnvironment.DEFAULT,
)
-client.meeting.participants(
- id="id",
+client.meeting.create_link(
+ name="Q1 Planning Session",
+ host="alex.chen@example.com",
+ start=datetime.datetime.fromisoformat("2026-02-15T14:00:00+00:00"),
+ end=datetime.datetime.fromisoformat("2026-02-15T15:00:00+00:00"),
)
-```
-
-
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**name:** `str` — Meeting Name
+
-#### ⚙️ Parameters
-
-
+**host:** `typing.Optional[str]`
+
+Meeting Host Email, matching a member of your Roam.
+
+Required 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.
+
+
+
+
-
-**id:** `str` — The meeting ID.
+**start:** `typing.Optional[datetime.datetime]` — (Optional) Meeting start time in RFC3339.
@@ -4127,7 +5099,7 @@ client.meeting.participants(
-
-**limit:** `typing.Optional[int]` — Number of participants to return per page (default 50, max 200).
+**end:** `typing.Optional[datetime.datetime]` — (Optional) Meeting end time in RFC3339.
@@ -4135,7 +5107,7 @@ client.meeting.participants(
-
-**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not parse or construct cursors yourself.
+**require_unconfirmed_email:** `typing.Optional[bool]` — (Optional) If true, guests must verify ownership of their email address before joining.
@@ -4155,7 +5127,7 @@ client.meeting.participants(
-client.meeting.transcript(...) -> TranscriptMeetingResponse
+client.meeting.link_info(...) -> LinkInfoMeetingResponse
-
@@ -4167,24 +5139,11 @@ client.meeting.participants(
-
-Retrieve the transcript for a meeting.
-
-Supports content negotiation:
-- **JSON** (default): Returns structured transcript with cues containing speaker IDs, text, and timing
-- **WebVTT**: Set `Accept: text/vtt` header to receive standard WebVTT format with speaker voice tags
-
-**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.
-
-**Required scope:** `meetings:read`
+Get a meeting link.
-**Errors** (see [Responses and Errors](https://developer.ro.am/docs/guides/responses-and-errors)):
+**Access:** Organization and Personal. Personal tokens may only read meeting links where the authenticated user is the host.
-| `error` code | Meaning |
-|--------------|---------|
-| `meeting_not_found` | Unknown or inaccessible meeting |
-| `transcript_pending` | Not ready yet — retry later (may include `Retry-After`) |
-| `transcript_unavailable` | Meeting was not transcribed — stop retrying |
-| `upstream_timeout` | Timed out waiting on an upstream service — retry |
+**Required scope:** `meetinglink:read`
@@ -4207,8 +5166,8 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.meeting.transcript(
- id="id",
+client.meeting.link_info(
+ id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
)
```
@@ -4225,7 +5184,7 @@ client.meeting.transcript(
-
-**id:** `str` — The meeting ID.
+**id:** `str` — Meeting Link ID
@@ -4245,7 +5204,7 @@ client.meeting.transcript(
-client.meeting.search(...) -> SearchMeetingResponse
+client.meeting.update_link(...)
-
@@ -4257,11 +5216,11 @@ client.meeting.transcript(
-
-AI-powered search across meeting transcripts and summaries.
+Update a meeting link.
-**Access:** Personal access only. Organization (account-level) tokens are not supported.
+**Access:** Organization and Personal. Personal tokens may only update meeting links where the authenticated user is the host.
-**Required scope:** `meetings:read`
+**Required scope:** `meetinglink:write`
@@ -4278,14 +5237,18 @@ AI-powered search across meeting transcripts and summaries.
```python
from roamhq import RoamClient
from roamhq.environment import RoamClientEnvironment
+import datetime
client = RoamClient(
token="",
environment=RoamClientEnvironment.DEFAULT,
)
-client.meeting.search(
- query="query",
+client.meeting.update_link(
+ id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ name="Q1 Planning Session - Updated",
+ start=datetime.datetime.fromisoformat("2026-02-15T15:00:00+00:00"),
+ end=datetime.datetime.fromisoformat("2026-02-15T16:30:00+00:00"),
)
```
@@ -4302,7 +5265,7 @@ client.meeting.search(
-
-**query:** `str` — Search query string.
+**id:** `str` — Meeting Link ID
@@ -4310,7 +5273,7 @@ client.meeting.search(
-
-**after:** `typing.Optional[datetime.date]` — Only return results from meetings after this date (YYYY-MM-DD).
+**name:** `str` — Meeting Name
@@ -4318,7 +5281,13 @@ client.meeting.search(
-
-**before:** `typing.Optional[datetime.date]` — Only return results from meetings before this date (YYYY-MM-DD).
+**host:** `typing.Optional[str]`
+
+(Optional) Meeting Host Email.
+
+The Host may NOT be updated.
+As a result, this property may be omitted or empty.
+If it is provided, it MUST match the existing value.
@@ -4326,7 +5295,23 @@ client.meeting.search(
-
-**timezone:** `typing.Optional[str]` — Timezone for date interpretation (e.g. "America/New_York").
+**start:** `typing.Optional[datetime.datetime]` — (Optional) Meeting start time in RFC3339.
+
+
+
+
+
+-
+
+**end:** `typing.Optional[datetime.datetime]` — (Optional) Meeting end time in RFC3339.
+
+
+
+
+
+-
+
+**require_unconfirmed_email:** `typing.Optional[bool]` — (Optional) If true, guests must verify ownership of their email address before joining.
@@ -4346,7 +5331,8 @@ client.meeting.search(
-client.meeting.prompt(...) -> PromptMeetingResponse
+## Meetings
+client.meetings.recording_list(...) -> RecordingListResponse
-
@@ -4358,11 +5344,32 @@ client.meeting.search(
-
-Ask an AI question about a meeting's transcript content. Returns a natural language response based on the meeting transcript.
+**Legacy:** Prefer [`/meeting.list`](https://developer.ro.am/docs/api/meeting-list) /
+[`/meeting.info`](https://developer.ro.am/docs/api/meeting-info) for new integrations.
-**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.
+Lists recordings in your home Roam, filtered by date range (after/before).
+Organization clients without roam-wide meeting access
+([`admin:meetings:read`](https://developer.ro.am/docs/guides/scopes#meeting-width-adminmeetingsread))
+receive `403`; use [`/meeting.list`](https://developer.ro.am/docs/api/meeting-list) instead.
+This route remains registered for existing callers. It returns v0-style
+identifiers and is not a v1 media-download path.
-**Required scope:** `meetings:read`
+The plural alias `/recordings.list` is also registered for existing callers;
+use this singular form in new documentation and tooling.
+
+The ordering of results depends on the filter specified:
+
+- When no parameters are provided, the most recent recordings are returned,
+ sorted in reverse chronological order. This is equivalent to specifying `before`
+ as NOW and leaving `after` unspecified.
+
+- If `after` is specified, the results are sorted in forward chronological order.
+
+Either dates or datetimes may be specified. Dates are interpreted in UTC.
+
+**Access:** Organization only. Requires roam-wide meeting access.
+
+**Required scope:** `recordings:read` and `admin:meetings:read` (or a grandfathered roam-wide API key)
@@ -4385,10 +5392,7 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.meeting.prompt(
- id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
- prompt="What action items were assigned to Alex?",
-)
+client.meetings.recording_list()
```
@@ -4404,7 +5408,10 @@ client.meeting.prompt(
-
-**id:** `str` — The meeting ID.
+**after:** `typing.Optional[str]`
+
+The datetime to begin listing recordings (YYYY-MM-DD or RFC-3339).
+Defaults to "no filter".
@@ -4412,7 +5419,26 @@ client.meeting.prompt(
-
-**prompt:** `str` — The question to ask about the meeting.
+**before:** `typing.Optional[str]`
+
+The datetime until which to list recordings (YYYY-MM-DD or RFC-3339).
+Defaults to "now".
+
+
+
+
+
+-
+
+**limit:** `typing.Optional[int]` — The number of recordings to return per response. Default is 10.
+
+
+
+
+
+-
+
+**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
@@ -4432,7 +5458,8 @@ client.meeting.prompt(
-client.meeting.share_link(...) -> ShareLinkMeetingResponse
+## Calendar
+client.calendar.create_event(...) -> CreateEventCalendarResponse
-
@@ -4444,15 +5471,24 @@ client.meeting.prompt(
-
-Returns a shareable URL for a meeting that you can distribute to others. Pass the `id` of a meeting obtained from [`/meeting.list`](https://developer.ro.am/docs/api/meeting-list) or [`/meeting.info`](https://developer.ro.am/docs/api/meeting-info).
+Create a calendar event on the host's connected calendar. A Roam meeting link
+is automatically attached and email notifications are sent to attendees.
-This 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.
+The event is written to the first active, writable calendar associated with the
+host. The host must have a connected calendar provider (e.g. Google, Microsoft).
-Creating 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.
+**Recurring events:** Provide `rrule` to create a recurring series. A
+`timeZone` is required for recurring events.
-**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.
+**All-day events:** Set `allDay: true`; `start` and `end` are interpreted as
+dates and normalized to UTC midnight.
-**Required scope:** `meetings:read`
+**Access:** Organization and Personal. For Organization tokens, the `host` email
+is required and identifies the calendar owner. For Personal tokens, `host`
+defaults to the authenticated user; if provided it must match the
+authenticated user's email.
+
+**Required scope:** `calendar:write`
@@ -4469,14 +5505,24 @@ Creating a share link is a deliberate action, which is why it has its own endpoi
```python
from roamhq import RoamClient
from roamhq.environment import RoamClientEnvironment
+import datetime
client = RoamClient(
token="",
environment=RoamClientEnvironment.DEFAULT,
)
-client.meeting.share_link(
- id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+client.calendar.create_event(
+ title="Q1 Planning",
+ description="Plan Q1 roadmap",
+ start=datetime.datetime.fromisoformat("2026-02-15T14:00:00+00:00"),
+ end=datetime.datetime.fromisoformat("2026-02-15T15:00:00+00:00"),
+ time_zone="America/Los_Angeles",
+ attendees=[
+ "sam@example.com",
+ "Alex Doe "
+ ],
+ host="host@example.com",
)
```
@@ -4493,7 +5539,7 @@ client.meeting.share_link(
-
-**id:** `str` — The meeting ID.
+**title:** `str` — Event title.
@@ -4501,80 +5547,31 @@ client.meeting.share_link(
-
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+**start:** `datetime.datetime` — Event start time (RFC3339). For all-day events, the date portion is used.
-
-
-
-
-
-
-
-
-client.meeting.create_link(...) -> CreateLinkMeetingResponse
-
--
-
-#### 📝 Description
-
-
--
-
-Create a meeting link.
-
-**Access:** Organization and Personal. In Organization mode, specify the host by email. In Personal mode, the host defaults to the authenticated user.
-
-**Required scope:** `meeting:write` or `meetinglink:write`
-
-
+**end:** `datetime.datetime` — Event end time (RFC3339). For all-day events, the date portion is used.
+
-#### 🔌 Usage
-
-
--
-
-
-```python
-from roamhq import RoamClient
-from roamhq.environment import RoamClientEnvironment
-import datetime
-
-client = RoamClient(
- token="",
- environment=RoamClientEnvironment.DEFAULT,
-)
-
-client.meeting.create_link(
- name="Q1 Planning Session",
- host="alex.chen@example.com",
- start=datetime.datetime.fromisoformat("2026-02-15T14:00:00+00:00"),
- end=datetime.datetime.fromisoformat("2026-02-15T15:00:00+00:00"),
-)
-
-```
-
-
-
-
-
-#### ⚙️ Parameters
-
-
--
+**description:** `typing.Optional[str]` — (Optional) Event description.
+
+
+
-
-**name:** `str` — Meeting Name
+**all_day:** `typing.Optional[bool]` — Whether this is an all-day event. Defaults to false.
@@ -4582,11 +5579,10 @@ client.meeting.create_link(
-
-**host:** `typing.Optional[str]`
-
-Meeting Host Email, matching a member of your Roam.
+**rrule:** `typing.Optional[str]`
-Required 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.
+(Optional) iCalendar RFC 5545 recurrence rule, e.g. `FREQ=WEEKLY;COUNT=10`.
+When provided, `timeZone` is required.
@@ -4594,7 +5590,10 @@ Required for Organization tokens. For Personal tokens, this is optional and defa
-
-**start:** `typing.Optional[datetime.datetime]` — (Optional) Meeting start time in RFC3339.
+**time_zone:** `typing.Optional[str]`
+
+IANA timezone name, e.g. `America/New_York`. Required for recurring
+events; recommended for all events. Defaults to `UTC` when omitted.
@@ -4602,7 +5601,10 @@ Required for Organization tokens. For Personal tokens, this is optional and defa
-
-**end:** `typing.Optional[datetime.datetime]` — (Optional) Meeting end time in RFC3339.
+**attendees:** `typing.Optional[typing.List[str]]`
+
+Attendee email addresses. Each entry may be a plain email
+(`user@example.com`) or an address string (`Name `).
@@ -4610,7 +5612,11 @@ Required for Organization tokens. For Personal tokens, this is optional and defa
-
-**require_unconfirmed_email:** `typing.Optional[bool]` — (Optional) If true, guests must verify ownership of their email address before joining.
+**host:** `typing.Optional[str]`
+
+Calendar host email. Required for Organization tokens. For Personal
+tokens, defaults to the authenticated user and, if provided, must
+match the authenticated user's email.
@@ -4630,7 +5636,7 @@ Required for Organization tokens. For Personal tokens, this is optional and defa
-client.meeting.link_info(...) -> LinkInfoMeetingResponse
+client.calendar.list(...) -> ListCalendarResponse
-
@@ -4642,11 +5648,27 @@ Required for Organization tokens. For Personal tokens, this is optional and defa
-
-Get a meeting link.
+List events from the authenticated user's connected calendars within
+a date range.
-**Access:** Organization and Personal. Personal tokens may only read meeting links where the authenticated user is the host.
+Pulls events from every active personal calendar attached to the user
+(e.g. Google, Microsoft) and merges them into a single chronological
+list. Canceled events are omitted.
-**Required scope:** `meetinglink:read`
+**Date range:** Defaults to a 7-day window starting today (caller's
+timezone). Pass `startDate` to shift the window's start; pass
+`endDate` to set its end (inclusive). Both are interpreted as
+`YYYY-MM-DD` in the caller's timezone.
+
+**Access:** Personal access only. Organization tokens do not have
+access to individual calendars and receive a `400`.
+
+**Required scope:** `calendar:read`
+
+`meetings:read` also grants this endpoint, but only for API clients
+registered **before 2026-07-29T00:00Z**. Clients registered on or after that
+date must hold `calendar:read`, or the call fails with `403` /
+`missing_scope`. See [Scopes](https://developer.ro.am/docs/guides/scopes).
@@ -4669,9 +5691,7 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.meeting.link_info(
- id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
-)
+client.calendar.list()
```
@@ -4687,7 +5707,18 @@ client.meeting.link_info(
-
-**id:** `str` — Meeting Link ID
+**start_date:** `typing.Optional[str]` — First day to include (`YYYY-MM-DD`, caller's timezone). Defaults to today.
+
+
+
+
+
+-
+
+**end_date:** `typing.Optional[str]`
+
+Last day to include (`YYYY-MM-DD`, caller's timezone, inclusive).
+Defaults to seven days after the resolved `startDate`.
@@ -4707,7 +5738,8 @@ client.meeting.link_info(
-client.meeting.update_link(...)
+## Lobby
+client.lobby.list(...) -> ListLobbyResponse
-
@@ -4719,11 +5751,23 @@ client.meeting.link_info(
-
-Update a meeting link.
+Lists active lobbies in your account.
-**Access:** Organization and Personal. Personal tokens may only update meeting links where the authenticated user is the host.
+A lobby URL has the form `ro.am/{handle}` or `ro.am/{handle}/{slug}`.
+- The "handle" is the first path segment
+- The "slug" is the optional second path segment. It may be empty for the default lobby under a handle
-**Required scope:** `meetinglink:write`
+Optionally filter by a specific lobby handle. If provided, only lobbies
+associated with that handle are returned.
+
+This endpoint is **not paginated**. The 200 body is `{ "lobbies": [...] }`
+with every matching lobby; there is no `cursor` / `nextCursor` and no
+`data` array. The TypeScript SDK returns that object directly, not a
+page helper.
+
+**Access:** Organization and Personal.
+
+**Required scope:** `lobby:read`
@@ -4740,19 +5784,13 @@ Update a meeting link.
```python
from roamhq import RoamClient
from roamhq.environment import RoamClientEnvironment
-import datetime
client = RoamClient(
token="",
environment=RoamClientEnvironment.DEFAULT,
)
-client.meeting.update_link(
- id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
- name="Q1 Planning Session - Updated",
- start=datetime.datetime.fromisoformat("2026-02-15T15:00:00+00:00"),
- end=datetime.datetime.fromisoformat("2026-02-15T16:30:00+00:00"),
-)
+client.lobby.list()
```
@@ -4768,53 +5806,10 @@ client.meeting.update_link(
-
-**id:** `str` — Meeting Link ID
-
-
-
-
-
--
-
-**name:** `str` — Meeting Name
-
-
-
-
-
--
-
-**host:** `typing.Optional[str]`
-
-(Optional) Meeting Host Email.
-
-The Host may NOT be updated.
-As a result, this property may be omitted or empty.
-If it is provided, it MUST match the existing value.
-
-
-
-
-
--
-
-**start:** `typing.Optional[datetime.datetime]` — (Optional) Meeting start time in RFC3339.
-
-
-
-
-
--
-
-**end:** `typing.Optional[datetime.datetime]` — (Optional) Meeting end time in RFC3339.
-
-
-
-
-
--
+**handle:** `typing.Optional[str]`
-**require_unconfirmed_email:** `typing.Optional[bool]` — (Optional) If true, guests must verify ownership of their email address before joining.
+Filter by lobby handle (first path segment), e.g., `robfig` for
+`ro.am/robfig` or `ro.am/robfig/tour`.
@@ -4834,8 +5829,7 @@ If it is provided, it MUST match the existing value.
-## Meetings
-client.meetings.recording_list(...) -> RecordingListResponse
+client.lobby.list_bookings(...) -> ListBookingsLobbyResponse
-
@@ -4847,22 +5841,11 @@ If it is provided, it MUST match the existing value.
-
-**Legacy:** Prefer [`/meeting.list`](https://developer.ro.am/docs/api/meeting-list) /
-[`/meeting.info`](https://developer.ro.am/docs/api/meeting-info) for new integrations.
-
-Lists recordings in your home Roam, filtered by date range (after/before).
-Organization clients without roam-wide meeting access
-([`admin:meetings:read`](https://developer.ro.am/docs/guides/scopes#meeting-width-adminmeetingsread))
-receive `403`; use [`/meeting.list`](https://developer.ro.am/docs/api/meeting-list) instead.
-This route remains registered for existing callers. It returns v0-style
-identifiers and is not a v1 media-download path.
-
-The plural alias `/recordings.list` is also registered for existing callers;
-use this singular form in new documentation and tooling.
+Lists bookings for a specific lobby configuration, filtered by date range (after/before).
The ordering of results depends on the filter specified:
-- When no parameters are provided, the most recent recordings are returned,
+- When no parameters are provided, the most recent bookings are returned,
sorted in reverse chronological order. This is equivalent to specifying `before`
as NOW and leaving `after` unspecified.
@@ -4870,9 +5853,9 @@ The ordering of results depends on the filter specified:
Either dates or datetimes may be specified. Dates are interpreted in UTC.
-**Access:** Organization only. Requires roam-wide meeting access.
+**Access:** Organization and Personal.
-**Required scope:** `recordings:read` and `admin:meetings:read` (or a grandfathered roam-wide API key)
+**Required scope:** `lobby:read`
@@ -4895,7 +5878,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.meetings.recording_list()
+client.lobby.list_bookings(
+ lobby_id="lobbyId",
+)
```
@@ -4911,9 +5896,17 @@ client.meetings.recording_list()
-
-**after:** `typing.Optional[str]`
+**lobby_id:** `str` — The lobby configuration ID to list bookings for.
+
+
+
-The datetime to begin listing recordings (YYYY-MM-DD or RFC-3339).
+
+-
+
+**after:** `typing.Optional[datetime.datetime]`
+
+The datetime to begin listing bookings (YYYY-MM-DD or RFC-3339).
Defaults to "no filter".
@@ -4922,9 +5915,9 @@ Defaults to "no filter".
-
-**before:** `typing.Optional[str]`
+**before:** `typing.Optional[datetime.datetime]`
-The datetime until which to list recordings (YYYY-MM-DD or RFC-3339).
+The datetime until which to list bookings (YYYY-MM-DD or RFC-3339).
Defaults to "now".
@@ -4933,7 +5926,7 @@ Defaults to "now".
-
-**limit:** `typing.Optional[int]` — The number of recordings to return per response. Default is 10.
+**limit:** `typing.Optional[int]` — The number of bookings to return per response. Default is 10.
@@ -4961,8 +5954,8 @@ Defaults to "now".
-## Calendar
-client.calendar.create_event(...) -> CreateEventCalendarResponse
+## Magicast
+client.magicast.list(...) -> ListMagicastResponse
-
@@ -4974,24 +5967,17 @@ Defaults to "now".
-
-Create a calendar event on the host's connected calendar. A Roam meeting link
-is automatically attached and email notifications are sent to attendees.
-
-The event is written to the first active, writable calendar associated with the
-host. The host must have a connected calendar provider (e.g. Google, Microsoft).
-
-**Recurring events:** Provide `rrule` to create a recurring series. A
-`timeZone` is required for recurring events.
+List Magicasts in your account, most recent first.
-**All-day events:** Set `allDay: true`; `start` and `end` are interpreted as
-dates and normalized to UTC midnight.
+Returns metadata only (`id`, `name`, `createdAt`, `ownerId`,
+`coverImageUrl`). Use [`/magicast.info`](https://developer.ro.am/docs/api/magicast-info) for
+transcript cues, chapters, video status, and a signed download URL.
-**Access:** Organization and Personal. For Organization tokens, the `host` email
-is required and identifies the calendar owner. For Personal tokens, `host`
-defaults to the authenticated user; if provided it must match the
-authenticated user's email.
+**Access:** Organization and Personal. Organization tokens list every
+Magicast in the account, including ones the creator never shared. Personal
+tokens are restricted to Magicasts owned by the authenticated user.
-**Required scope:** `calendar:write`
+**Required scope:** `magicast:read`
@@ -5005,76 +5991,32 @@ authenticated user's email.
-
-```python
-from roamhq import RoamClient
-from roamhq.environment import RoamClientEnvironment
-import datetime
-
-client = RoamClient(
- token="",
- environment=RoamClientEnvironment.DEFAULT,
-)
-
-client.calendar.create_event(
- title="Q1 Planning",
- description="Plan Q1 roadmap",
- start=datetime.datetime.fromisoformat("2026-02-15T14:00:00+00:00"),
- end=datetime.datetime.fromisoformat("2026-02-15T15:00:00+00:00"),
- time_zone="America/Los_Angeles",
- attendees=[
- "sam@example.com",
- "Alex Doe "
- ],
- host="host@example.com",
-)
-
-```
-
-
-
-
-
-#### ⚙️ Parameters
-
-
--
-
-
--
-
-**title:** `str` — Event title.
-
-
-
+```python
+from roamhq import RoamClient
+from roamhq.environment import RoamClientEnvironment
-
--
+client = RoamClient(
+ token="",
+ environment=RoamClientEnvironment.DEFAULT,
+)
-**start:** `datetime.datetime` — Event start time (RFC3339). For all-day events, the date portion is used.
-
+client.magicast.list()
+
+```
-
-
--
-
-**end:** `datetime.datetime` — Event end time (RFC3339). For all-day events, the date portion is used.
-
+#### ⚙️ Parameters
+
-
-**description:** `typing.Optional[str]` — (Optional) Event description.
-
-
-
-
-
-**all_day:** `typing.Optional[bool]` — Whether this is an all-day event. Defaults to false.
+**after:** `typing.Optional[datetime.datetime]` — Only return magicasts created after this time (RFC-3339).
@@ -5082,10 +6024,7 @@ client.calendar.create_event(
-
-**rrule:** `typing.Optional[str]`
-
-(Optional) iCalendar RFC 5545 recurrence rule, e.g. `FREQ=WEEKLY;COUNT=10`.
-When provided, `timeZone` is required.
+**before:** `typing.Optional[datetime.datetime]` — Only return magicasts created before this time (RFC-3339).
@@ -5093,10 +6032,7 @@ When provided, `timeZone` is required.
-
-**time_zone:** `typing.Optional[str]`
-
-IANA timezone name, e.g. `America/New_York`. Required for recurring
-events; recommended for all events. Defaults to `UTC` when omitted.
+**ascending:** `typing.Optional[bool]` — Sort oldest-first instead of newest-first.
@@ -5104,10 +6040,7 @@ events; recommended for all events. Defaults to `UTC` when omitted.
-
-**attendees:** `typing.Optional[typing.List[str]]`
-
-Attendee email addresses. Each entry may be a plain email
-(`user@example.com`) or an address string (`Name `).
+**limit:** `typing.Optional[int]` — Number of magicasts to return per response. Default 10.
@@ -5115,11 +6048,7 @@ Attendee email addresses. Each entry may be a plain email
-
-**host:** `typing.Optional[str]`
-
-Calendar host email. Required for Organization tokens. For Personal
-tokens, defaults to the authenticated user and, if provided, must
-match the authenticated user's email.
+**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
@@ -5139,7 +6068,7 @@ match the authenticated user's email.
-client.calendar.list(...) -> ListCalendarResponse
+client.magicast.info(...) -> MagicastInfo
-
@@ -5151,27 +6080,29 @@ match the authenticated user's email.
-
-List events from the authenticated user's connected calendars within
-a date range.
+Get details for a single Magicast by ID, including transcript cues,
+chapters, video status, a signed video download URL when ready, and a
+player URL if a share link already exists.
-Pulls events from every active personal calendar attached to the user
-(e.g. Google, Microsoft) and merges them into a single chronological
-list. Canceled events are omitted.
+This is the content endpoint. [`/magicast.list`](https://developer.ro.am/docs/api/magicast-list)
+returns metadata only. Magicasts are not meetings — they do not appear on
+[`/recording.list`](https://developer.ro.am/docs/api/recording-list) or meeting transcript
+surfaces, and they have no Magic Minutes summary or action items.
-**Date range:** Defaults to a 7-day window starting today (caller's
-timezone). Pass `startDate` to shift the window's start; pass
-`endDate` to set its end (inclusive). Both are interpreted as
-`YYYY-MM-DD` in the caller's timezone.
+Asset, transcript, and share-link lookups are best-effort. If the video or
+transcript is still processing, those fields are omitted and the request
+still succeeds. Fetching this endpoint **never** mints a shareable link;
+use [`/magicast.shareLink`](https://developer.ro.am/docs/api/magicast-share-link) for that.
-**Access:** Personal access only. Organization tokens do not have
-access to individual calendars and receive a `400`.
+There is no `https://ro.am/magicast/{id}` browser URL. The player URL is
+always `https://ro.am/share/{key}`.
-**Required scope:** `calendar:read`
+**Access:** Organization and Personal. Organization tokens can read every
+Magicast in the account, including ones the creator never shared. Personal
+tokens are restricted to Magicasts owned by the authenticated user. Filter
+on whether `shareUrl` is present if you only want shared recordings.
-`meetings:read` also grants this endpoint, but only for API clients
-registered **before 2026-07-29T00:00Z**. Clients registered on or after that
-date must hold `calendar:read`, or the call fails with `403` /
-`missing_scope`. See [Scopes](https://developer.ro.am/docs/guides/scopes).
+**Required scope:** `magicast:read`
@@ -5194,7 +6125,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.calendar.list()
+client.magicast.info(
+ id="id",
+)
```
@@ -5210,18 +6143,7 @@ client.calendar.list()
-
-**start_date:** `typing.Optional[str]` — First day to include (`YYYY-MM-DD`, caller's timezone). Defaults to today.
-
-
-
-
-
--
-
-**end_date:** `typing.Optional[str]`
-
-Last day to include (`YYYY-MM-DD`, caller's timezone, inclusive).
-Defaults to seven days after the resolved `startDate`.
+**id:** `str` — The magicast ID.
@@ -5241,8 +6163,8 @@ Defaults to seven days after the resolved `startDate`.
-## Lobby
-client.lobby.list(...) -> ListLobbyResponse
+## Magicasts
+client.magicasts.magicast_share_link(...) -> MagicastShareLinkResponse
-
@@ -5254,23 +6176,30 @@ Defaults to seven days after the resolved `startDate`.
-
-Lists active lobbies in your account.
+Returns a shareable player URL for a Magicast. Pass the `id` obtained from
+[`/magicast.list`](https://developer.ro.am/docs/api/magicast-list) or
+[`/magicast.info`](https://developer.ro.am/docs/api/magicast-info).
-A lobby URL has the form `ro.am/{handle}` or `ro.am/{handle}/{slug}`.
-- The "handle" is the first path segment
-- The "slug" is the optional second path segment. It may be empty for the default lobby under a handle
+This endpoint is **get-or-create**: it returns the Magicast's existing
+share link, or mints one the first time it is called. Repeat calls for the
+same Magicast return the same URL.
-Optionally filter by a specific lobby handle. If provided, only lobbies
-associated with that handle are returned.
+Creating a share link is a deliberate action, which is why it has its own
+endpoint rather than being returned as a field that is always present on
+`magicast.list` / `magicast.info`. Fetching a Magicast never mints a
+shareable link as a side effect. `magicast.info` includes `shareUrl` only
+when a link already exists.
-This endpoint is **not paginated**. The 200 body is `{ "lobbies": [...] }`
-with every matching lobby; there is no `cursor` / `nextCursor` and no
-`data` array. The TypeScript SDK returns that object directly, not a
-page helper.
+The URL is `https://ro.am/share/{key}`. There is no
+`https://ro.am/magicast/{id}` route.
-**Access:** Organization and Personal.
+You can only create a share link for a Magicast you can access; the same
+access check as `magicast.info` applies.
-**Required scope:** `lobby:read`
+**Access:** Organization and Personal. Personal access tokens restrict to
+Magicasts owned by the authenticated user.
+
+**Required scope:** `magicast:read`
@@ -5293,7 +6222,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.lobby.list()
+client.magicasts.magicast_share_link(
+ id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+)
```
@@ -5309,10 +6240,7 @@ client.lobby.list()
-
-**handle:** `typing.Optional[str]`
-
-Filter by lobby handle (first path segment), e.g., `robfig` for
-`ro.am/robfig` or `ro.am/robfig/tour`.
+**id:** `str` — The Magicast ID.
@@ -5332,7 +6260,8 @@ Filter by lobby handle (first path segment), e.g., `robfig` for
-client.lobby.list_bookings(...) -> ListBookingsLobbyResponse
+## Group
+client.group.list(...) -> ListGroupResponse
-
@@ -5344,21 +6273,14 @@ Filter by lobby handle (first path segment), e.g., `robfig` for
-
-Lists bookings for a specific lobby configuration, filtered by date range (after/before).
-
-The ordering of results depends on the filter specified:
-
-- When no parameters are provided, the most recent bookings are returned,
- sorted in reverse chronological order. This is equivalent to specifying `before`
- as NOW and leaving `after` unspecified.
-
-- If `after` is specified, the results are sorted in forward chronological order.
+Lists non-archived groups accessible to the caller.
-Either dates or datetimes may be specified. Dates are interpreted in UTC.
+Filter by name with `query` (ranked text match), restrict by group
+type with `type`, and paginate with `limit` / `cursor`.
**Access:** Organization and Personal.
-**Required scope:** `lobby:read`
+**Required scope:** `group:read`
@@ -5381,9 +6303,7 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.lobby.list_bookings(
- lobby_id="lobbyId",
-)
+client.group.list()
```
@@ -5399,18 +6319,7 @@ client.lobby.list_bookings(
-
-**lobby_id:** `str` — The lobby configuration ID to list bookings for.
-
-
-
-
-
--
-
-**after:** `typing.Optional[datetime.datetime]`
-
-The datetime to begin listing bookings (YYYY-MM-DD or RFC-3339).
-Defaults to "no filter".
+**query:** `typing.Optional[str]` — Text filter. Groups are ranked by how well their name matches the query.
@@ -5418,10 +6327,11 @@ Defaults to "no filter".
-
-**before:** `typing.Optional[datetime.datetime]`
+**type:** `typing.Optional[str]`
-The datetime until which to list bookings (YYYY-MM-DD or RFC-3339).
-Defaults to "now".
+Comma-separated list of group types to include. Must be one or
+more of `standard`, `magicast`, `meeting`, `roam`, `onair`.
+Defaults to all types.
@@ -5429,7 +6339,7 @@ Defaults to "now".
-
-**limit:** `typing.Optional[int]` — The number of bookings to return per response. Default is 10.
+**limit:** `typing.Optional[int]` — Number of groups to return per page (default 50, max 100).
@@ -5457,8 +6367,7 @@ Defaults to "now".
-## Magicast
-client.magicast.list(...) -> ListMagicastResponse
+client.group.info(...) -> Group
-
@@ -5470,17 +6379,11 @@ Defaults to "now".
-
-List Magicasts in your account, most recent first.
-
-Returns metadata only (`id`, `name`, `createdAt`, `ownerId`,
-`coverImageUrl`). Use [`/magicast.info`](https://developer.ro.am/docs/api/magicast-info) for
-transcript cues, chapters, video status, and a signed download URL.
+Get information about a specific group by its ID or name.
-**Access:** Organization and Personal. Organization tokens list every
-Magicast in the account, including ones the creator never shared. Personal
-tokens are restricted to Magicasts owned by the authenticated user.
+Provide either `id` or `name`, not both.
-**Required scope:** `magicast:read`
+**Required scope:** `group:read`
@@ -5503,7 +6406,7 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.magicast.list()
+client.group.info()
```
@@ -5519,31 +6422,7 @@ client.magicast.list()
-
-**after:** `typing.Optional[datetime.datetime]` — Only return magicasts created after this time (RFC-3339).
-
-
-
-
-
--
-
-**before:** `typing.Optional[datetime.datetime]` — Only return magicasts created before this time (RFC-3339).
-
-
-
-
-
--
-
-**ascending:** `typing.Optional[bool]` — Sort oldest-first instead of newest-first.
-
-
-
-
-
--
-
-**limit:** `typing.Optional[int]` — Number of magicasts to return per response. Default 10.
+**id:** `typing.Optional[str]` — The group's ID. Mutually exclusive with `name`.
@@ -5551,7 +6430,7 @@ client.magicast.list()
-
-**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+**name:** `typing.Optional[str]` — The group's name. Mutually exclusive with `id`. Returns first match if multiple groups have the same name.
@@ -5571,41 +6450,30 @@ client.magicast.list()
-client.magicast.info(...) -> MagicastInfo
+client.group.create(...) -> Group
-
#### 📝 Description
-
--
-
-
--
-
-Get details for a single Magicast by ID, including transcript cues,
-chapters, video status, a signed video download URL when ready, and a
-player URL if a share link already exists.
-
-This is the content endpoint. [`/magicast.list`](https://developer.ro.am/docs/api/magicast-list)
-returns metadata only. Magicasts are not meetings — they do not appear on
-[`/recording.list`](https://developer.ro.am/docs/api/recording-list) or meeting transcript
-surfaces, and they have no Magic Minutes summary or action items.
+
+-
-Asset, transcript, and share-link lookups are best-effort. If the video or
-transcript is still processing, those fields are omitted and the request
-still succeeds. Fetching this endpoint **never** mints a shareable link;
-use [`/magicast.shareLink`](https://developer.ro.am/docs/api/magicast-share-link) for that.
+
+-
-There is no `https://ro.am/magicast/{id}` browser URL. The player URL is
-always `https://ro.am/share/{key}`.
+Create a group chat.
-**Access:** Organization and Personal. Organization tokens can read every
-Magicast in the account, including ones the creator never shared. Personal
-tokens are restricted to Magicasts owned by the authenticated user. Filter
-on whether `shareUrl` is present if you only want shared recordings.
+Groups which specify at least one admin will operate in an "Admin only" management
+mode, where only admins may change settings. Otherwise, all members have
+that capability.
-**Required scope:** `magicast:read`
+Groups require at least one member. Users can be specified by user ID or email address.
+Unrecognized emails are invited as group members only — they do not receive a
+[Guest Badge](https://developer.ro.am/docs/guides/guest-badges) unless you also call
+[`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create).
+
+**Required scope:** `group:write`
@@ -5622,14 +6490,32 @@ on whether `shareUrl` is present if you only want shared recordings.
```python
from roamhq import RoamClient
from roamhq.environment import RoamClientEnvironment
+from roamhq.group import CreateGroupRequestMembersItem
client = RoamClient(
token="",
environment=RoamClientEnvironment.DEFAULT,
)
-client.magicast.info(
- id="id",
+client.group.create(
+ name="Engineering Team",
+ description="Group chat for engineering discussions and updates",
+ private=False,
+ enforce_threads=True,
+ members=[
+ CreateGroupRequestMembersItem(
+ user_id="alex.chen@example.com",
+ role="member",
+ ),
+ CreateGroupRequestMembersItem(
+ user_id="taylor@example.com",
+ role="member",
+ ),
+ CreateGroupRequestMembersItem(
+ user_id="jordan.smith@example.com",
+ role="admin",
+ )
+ ],
)
```
@@ -5646,7 +6532,7 @@ client.magicast.info(
-
-**id:** `str` — The magicast ID.
+**name:** `str` — Name of the group
@@ -5654,55 +6540,67 @@ client.magicast.info(
-
-**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+**members:** `typing.List[CreateGroupRequestMembersItem]` — Group members with their roles
+
+
+-
+
+**description:** `typing.Optional[str]` — Description of the group
+
+
+-
+**private:** `typing.Optional[bool]` — Whether the group is private (default false)
+
-
-## Magicasts
-client.magicasts.magicast_share_link(...) -> MagicastShareLinkResponse
-
-#### 📝 Description
+**enforce_threads:** `typing.Optional[bool]` — Whether to enforce threaded conversations
+
+
+
-
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.group.rename(...)
-
-Returns a shareable player URL for a Magicast. Pass the `id` obtained from
-[`/magicast.list`](https://developer.ro.am/docs/api/magicast-list) or
-[`/magicast.info`](https://developer.ro.am/docs/api/magicast-info).
-
-This endpoint is **get-or-create**: it returns the Magicast's existing
-share link, or mints one the first time it is called. Repeat calls for the
-same Magicast return the same URL.
+#### 📝 Description
-Creating a share link is a deliberate action, which is why it has its own
-endpoint rather than being returned as a field that is always present on
-`magicast.list` / `magicast.info`. Fetching a Magicast never mints a
-shareable link as a side effect. `magicast.info` includes `shareUrl` only
-when a link already exists.
+
+-
-The URL is `https://ro.am/share/{key}`. There is no
-`https://ro.am/magicast/{id}` route.
+
+-
-You can only create a share link for a Magicast you can access; the same
-access check as `magicast.info` applies.
+Rename a group by ID.
-**Access:** Organization and Personal. Personal access tokens restrict to
-Magicasts owned by the authenticated user.
+Apps may only rename groups for which they are an admin.
-**Required scope:** `magicast:read`
+**Required scope:** `group:write`
@@ -5725,8 +6623,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.magicasts.magicast_share_link(
- id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+client.group.rename(
+ id="88bebce7-6cbb-4666-96f9-5c02d73e6661",
+ name="Product Engineering",
)
```
@@ -5743,7 +6642,15 @@ client.magicasts.magicast_share_link(
-
-**id:** `str` — The Magicast ID.
+**id:** `str` — The group ID
+
+
+
+
+
+-
+
+**name:** `str` — The new name for the group
@@ -5763,8 +6670,7 @@ client.magicasts.magicast_share_link(
-## Group
-client.group.list(...) -> ListGroupResponse
+client.group.archive(...)
-
@@ -5776,14 +6682,11 @@ client.magicasts.magicast_share_link(
-
-Lists non-archived groups accessible to the caller.
-
-Filter by name with `query` (ranked text match), restrict by group
-type with `type`, and paginate with `limit` / `cursor`.
+Archive a group by ID.
-**Access:** Organization and Personal.
+Apps may only archive groups for which they are an admin.
-**Required scope:** `group:read`
+**Required scope:** `group:write`
@@ -5806,7 +6709,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.group.list()
+client.group.archive(
+ id="88bebce7-6cbb-4666-96f9-5c02d73e6661",
+)
```
@@ -5822,35 +6727,7 @@ client.group.list()
-
-**query:** `typing.Optional[str]` — Text filter. Groups are ranked by how well their name matches the query.
-
-
-
-
-
--
-
-**type:** `typing.Optional[str]`
-
-Comma-separated list of group types to include. Must be one or
-more of `standard`, `magicast`, `meeting`, `roam`, `onair`.
-Defaults to all types.
-
-
-
-
-
--
-
-**limit:** `typing.Optional[int]` — Number of groups to return per page (default 50, max 100).
-
-
-
-
-
--
-
-**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
+**id:** `str` — The group ID to archive.
@@ -5870,7 +6747,7 @@ Defaults to all types.
-client.group.info(...) -> Group
+client.group.members(...) -> MembersGroupResponse
-
@@ -5882,11 +6759,17 @@ Defaults to all types.
-
-Get information about a specific group by its ID or name.
+List members in a group with their roles.
-Provide either `id` or `name`, not both.
+Apps may list members if one of the following conditions is true:
+1. It is a public group in their Roam.
+2. They are a member of the group.
**Required scope:** `group:read`
+
+Every returned `userId` is a visible principal ID that resolves through
+[`user.info`](https://developer.ro.am/docs/api/user-info) with the same credentials. Use
+`user.list?ids` for ordered bulk hydration.
@@ -5909,7 +6792,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.group.info()
+client.group.members(
+ id="id",
+)
```
@@ -5925,7 +6810,7 @@ client.group.info()
-
-**id:** `typing.Optional[str]` — The group's ID. Mutually exclusive with `name`.
+**id:** `str` — Group ID.
@@ -5933,7 +6818,15 @@ client.group.info()
-
-**name:** `typing.Optional[str]` — The group's name. Mutually exclusive with `id`. Returns first match if multiple groups have the same name.
+**limit:** `typing.Optional[int]` — The number of members to return per response. Default is 10.
+
+
+
+
+
+-
+
+**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
@@ -5953,7 +6846,7 @@ client.group.info()
-client.group.create(...) -> Group
+client.group.add(...)
-
@@ -5965,13 +6858,17 @@ client.group.info()
-
-Create a group chat.
+Add one or more group members with specified roles.
-Groups which specify at least one admin will operate in an "Admin only" management
-mode, where only admins may change settings. Otherwise, all members have
-that capability.
+Members can be specified by user ID or email address. Each member must be assigned a role (member or admin).
-Groups require at least one member. Users can be specified by user ID or email address.
+Adding an unrecognized email does **not** grant a [Guest Badge](https://developer.ro.am/docs/guides/guest-badges). Use [`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) first if the person is not a workspace member.
+
+Apps may add members to a group if one of the following conditions is true:
+1. It is a public group in their Roam.
+2. They are a member of the group.
+
+If attempting to add an admin, the app must be an admin of the group.
**Required scope:** `group:write`
@@ -5990,29 +6887,26 @@ Groups require at least one member. Users can be specified by user ID or email a
```python
from roamhq import RoamClient
from roamhq.environment import RoamClientEnvironment
-from roamhq.group import CreateGroupRequestMembersItem
+from roamhq.group import AddGroupRequestMembersItem
client = RoamClient(
token="",
environment=RoamClientEnvironment.DEFAULT,
)
-client.group.create(
- name="Engineering Team",
- description="Group chat for engineering discussions and updates",
- private=False,
- enforce_threads=True,
+client.group.add(
+ id="88bebce7-6cbb-4666-96f9-5c02d73e6661",
members=[
- CreateGroupRequestMembersItem(
- user_id="alex.chen@example.com",
+ AddGroupRequestMembersItem(
+ user_id="709b8a57-70bc-427a-b6f0-b16ba5297f8c",
role="member",
),
- CreateGroupRequestMembersItem(
- user_id="taylor@example.com",
+ AddGroupRequestMembersItem(
+ user_id="f589a8cb-78ac-493e-8719-0fa8a22f65e0",
role="member",
),
- CreateGroupRequestMembersItem(
- user_id="jordan.smith@example.com",
+ AddGroupRequestMembersItem(
+ user_id="af6663d5-0f37-4105-95df-4fea20ef7c7c",
role="admin",
)
],
@@ -6027,36 +6921,12 @@ client.group.create(
#### ⚙️ Parameters
--
-
-
--
-
-**name:** `str` — Name of the group
-
-
-
-
-
--
-
-**members:** `typing.List[CreateGroupRequestMembersItem]` — Group members with their roles
-
-
-
-
-
--
-
-**description:** `typing.Optional[str]` — Description of the group
-
-
-
+-
-
-**private:** `typing.Optional[bool]` — Whether the group is private (default false)
+**id:** `str` — Group ID
@@ -6064,7 +6934,7 @@ client.group.create(
-
-**enforce_threads:** `typing.Optional[bool]` — Whether to enforce threaded conversations
+**members:** `typing.Optional[typing.List[AddGroupRequestMembersItem]]` — List of members to add with their roles
@@ -6084,7 +6954,7 @@ client.group.create(
-client.group.rename(...)
+client.group.join(...) -> Group
-
@@ -6096,9 +6966,18 @@ client.group.create(
-
-Rename a group by ID.
+Join a public group as the calling identity (Slack `conversations.join`).
-Apps may only rename groups for which they are an admin.
+- Org tokens add the bot address as a member.
+- Personal tokens add the **owner person**, never the PAT bot address.
+- Private groups cannot be self-joined (`403`).
+- Idempotent if the calling identity is already a member.
+- Non-members of a group in another roam receive an opaque `403`
+ (`group_not_found`) — archived / type / privacy are not distinguished.
+
+Why join (webhooks vs history vs post): [Chat](https://developer.ro.am/docs/guides/chat).
+
+**Access:** Organization and Personal.
**Required scope:** `group:write`
@@ -6123,9 +7002,8 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.group.rename(
+client.group.join(
id="88bebce7-6cbb-4666-96f9-5c02d73e6661",
- name="Product Engineering",
)
```
@@ -6142,15 +7020,7 @@ client.group.rename(
-
-**id:** `str` — The group ID
-
-
-
-
-
--
-
-**name:** `str` — The new name for the group
+**id:** `str` — Group ID
@@ -6170,7 +7040,7 @@ client.group.rename(
-client.group.archive(...)
+client.group.remove(...)
-
@@ -6182,9 +7052,15 @@ client.group.rename(
-
-Archive a group by ID.
+Remove one or more group members.
-Apps may only archive groups for which they are an admin.
+Members can be specified by user ID or email address.
+
+Apps may remove members from a group if one of the following conditions is true:
+1. It is a public group in their Roam.
+2. They are a member of the group.
+
+Removing members with the Admin role is not yet supported.
**Required scope:** `group:write`
@@ -6209,8 +7085,11 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.group.archive(
+client.group.remove(
id="88bebce7-6cbb-4666-96f9-5c02d73e6661",
+ members=[
+ "709b8a57-70bc-427a-b6f0-b16ba5297f8c"
+ ],
)
```
@@ -6227,7 +7106,15 @@ client.group.archive(
-
-**id:** `str` — The group ID to archive.
+**id:** `str` — Group ID
+
+
+
+
+
+-
+
+**members:** `typing.List[str]` — List of member IDs or email addresses to remove
@@ -6247,7 +7134,8 @@ client.group.archive(
-client.group.members(...) -> MembersGroupResponse
+## Groups
+client.groups.list() -> typing.List[GroupsListResponseItem]
-
@@ -6259,17 +7147,17 @@ client.group.archive(
-
-List members in a group with their roles.
+**Legacy:** Prefer [`/group.list`](https://developer.ro.am/docs/api/group-list) for new integrations.
-Apps may list members if one of the following conditions is true:
-1. It is a public group in their Roam.
-2. They are a member of the group.
+Lists all public, non-archived groups in your home Roam.
-**Required scope:** `group:read`
+Unlike `/group.list`, this endpoint returns a **raw JSON array** (not the
+`{"ok": true, …}` envelope). It is the sole ok-envelope exception on `/v1`
+and remains only for existing callers.
-Every returned `userId` is a visible principal ID that resolves through
-[`user.info`](https://developer.ro.am/docs/api/user-info) with the same credentials. Use
-`user.list?ids` for ordered bulk hydration.
+**Access:** Organization only.
+
+**Required scope:** `group:read`
@@ -6292,9 +7180,7 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.group.members(
- id="id",
-)
+client.groups.list()
```
@@ -6310,30 +7196,6 @@ client.group.members(
-
-**id:** `str` — Group ID.
-
-
-
-
-
--
-
-**limit:** `typing.Optional[int]` — The number of members to return per response. Default is 10.
-
-
-
-
-
--
-
-**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
-
-
-
-
-
--
-
**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -6346,7 +7208,8 @@ client.group.members(
-client.group.add(...)
+## Guest Badges
+client.guest_badges.guest_badge_create(...) -> GuestBadge
-
@@ -6358,17 +7221,27 @@ client.group.members(
-
-Add one or more group members with specified roles.
+Grant a Guest Badge so an email that is **not** a workspace member can
+visit a host in Roam.
-Members can be specified by user ID or email address. Each member must be assigned a role (member or admin).
+This is **not** an [On-Air event guest](https://developer.ro.am/docs/onair-api/on-air-api). It is
+also **not** implied by [`group.add`](https://developer.ro.am/docs/api/group-add): adding an email
+to a group does not mint a badge or send the invite. Typical onboarding is
+`guest.badge.create` then `group.add`.
-Apps may add members to a group if one of the following conditions is true:
-1. It is a public group in their Roam.
-2. They are a member of the group.
+Repeating create for the same host and email returns the existing badge
+(`visitPermission` is **not** updated) and does not re-send the invite.
+To flip on-map access after create, use
+[`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update).
-If attempting to add an admin, the app must be an admin of the group.
+**Access:** Organization and Personal.
+Organization tokens require `hostUserId`. Personal tokens default to the
+token owner; naming a different host returns `403` `access_mode_not_supported`.
-**Required scope:** `group:write`
+**Required scope:** `guest:write`. Personal Access Tokens use the
+`pat:guests:write` group.
+
+See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
@@ -6385,29 +7258,15 @@ If attempting to add an admin, the app must be an admin of the group.
```python
from roamhq import RoamClient
from roamhq.environment import RoamClientEnvironment
-from roamhq.group import AddGroupRequestMembersItem
client = RoamClient(
token="",
environment=RoamClientEnvironment.DEFAULT,
)
-client.group.add(
- id="88bebce7-6cbb-4666-96f9-5c02d73e6661",
- members=[
- AddGroupRequestMembersItem(
- user_id="709b8a57-70bc-427a-b6f0-b16ba5297f8c",
- role="member",
- ),
- AddGroupRequestMembersItem(
- user_id="f589a8cb-78ac-493e-8719-0fa8a22f65e0",
- role="member",
- ),
- AddGroupRequestMembersItem(
- user_id="af6663d5-0f37-4105-95df-4fea20ef7c7c",
- role="admin",
- )
- ],
+client.guest_badges.guest_badge_create(
+ email="alex@client.example",
+ host_user_id="3f1c0b2a-8d4e-4c91-9a7b-2e6f1d8c0a11",
)
```
@@ -6424,7 +7283,7 @@ client.group.add(
-
-**id:** `str` — Group ID
+**email:** `str` — Guest email. ASCII only. Must not be a workspace member.
@@ -6432,7 +7291,25 @@ client.group.add(
-
-**members:** `typing.Optional[typing.List[AddGroupRequestMembersItem]]` — List of members to add with their roles
+**host_user_id:** `typing.Optional[str]`
+
+Host member. UUID or member email — the same convention as
+[`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+Required for organization tokens. Optional for personal tokens
+(defaults to the token owner).
+
+
+
+
+
+-
+
+**visit_permission:** `typing.Optional[bool]`
+
+Whether the guest may visit the host on the map. Defaults to
+`true`. Ignored on an idempotent retry of an existing grant
+— use [`guest.badge.update`](https://developer.ro.am/docs/api/guest-badge-update)
+to change it.
@@ -6452,7 +7329,7 @@ client.group.add(
-client.group.join(...) -> Group
+client.guest_badges.guest_badge_list(...) -> GuestBadgeListResponse
-
@@ -6464,20 +7341,29 @@ client.group.add(
-
-Join a public group as the calling identity (Slack `conversations.join`).
+List issued Guest Badges.
-- Org tokens add the bot address as a member.
-- Personal tokens add the **owner person**, never the PAT bot address.
-- Private groups cannot be self-joined (`403`).
-- Idempotent if the calling identity is already a member.
-- Non-members of a group in another roam receive an opaque `403`
- (`group_not_found`) — archived / type / privacy are not distinguished.
+Organization tokens return every issued badge in the workspace. Personal
+tokens return only badges the token owner issued. This is the issued
+(host) view — the same rows `guest.badge.create` returns — not the
+guest's hidden-inbox view.
-Why join (webhooks vs history vs post): [Chat](https://developer.ro.am/docs/guides/chat).
+Filter with `email` (alias-aware) and/or `hostUserId` (UUID or member
+email). Paginate with `limit` / `cursor` (default 50, max 100). Results
+are sorted by `(hostUserId, email)`.
+
+Organization keys that only have `guest:write` must also request
+`guest:read` to call list. Personal Access Tokens with `pat:guests:write`
+already include `guest:read`.
**Access:** Organization and Personal.
+Personal tokens naming a different host return `403`
+`access_mode_not_supported`.
-**Required scope:** `group:write`
+**Required scope:** `guest:read`. Personal Access Tokens use the
+`pat:guests:write` group.
+
+See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
@@ -6500,9 +7386,7 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.group.join(
- id="88bebce7-6cbb-4666-96f9-5c02d73e6661",
-)
+client.guest_badges.guest_badge_list()
```
@@ -6518,7 +7402,39 @@ client.group.join(
-
-**id:** `str` — Group ID
+**email:** `typing.Optional[str]`
+
+Guest email. ASCII only. Matches the stored address and its verified
+domain aliases.
+
+
+
+
+
+-
+
+**host_user_id:** `typing.Optional[str]`
+
+Host member. UUID or member email — the same convention as
+[`group.create`](https://developer.ro.am/docs/api/group-create) `members[].userId`.
+Archived hosts may be named (they typically have no remaining grants).
+Personal tokens may only pass the token owner.
+
+
+
+
+
+-
+
+**limit:** `typing.Optional[int]` — Number of badges to return per page (default 50, max 100).
+
+
+
+
+
+-
+
+**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually.
@@ -6538,7 +7454,7 @@ client.group.join(
-client.group.remove(...)
+client.guest_badges.guest_badge_update(...) -> GuestBadge
-
@@ -6550,17 +7466,27 @@ client.group.join(
-
-Remove one or more group members.
+Update `visitPermission` on an existing Guest Badge.
-Members can be specified by user ID or email address.
+[`guest.badge.create`](https://developer.ro.am/docs/api/guest-badge-create) is idempotent and
+does **not** change `visitPermission` on an existing grant. Use this
+endpoint to flip on-map visit access after create.
-Apps may remove members from a group if one of the following conditions is true:
-1. It is a public group in their Roam.
-2. They are a member of the group.
+If only one host in the workspace has granted this email, `hostUserId`
+may be omitted. If several hosts have, pass `hostUserId` to pick which
+grant to update (`400` `missing_parameter` otherwise). Same-host alias
+rows are updated together.
-Removing members with the Admin role is not yet supported.
+Personal tokens can only update badges they issued. Naming another host
+is `403` `access_mode_not_supported`; omitting `hostUserId` when the
+token owner has no matching grant is `404` `not_found`.
-**Required scope:** `group:write`
+**Access:** Organization and Personal.
+
+**Required scope:** `guest:write`. Personal Access Tokens use the
+`pat:guests:write` group.
+
+See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
@@ -6583,11 +7509,10 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.group.remove(
- id="88bebce7-6cbb-4666-96f9-5c02d73e6661",
- members=[
- "709b8a57-70bc-427a-b6f0-b16ba5297f8c"
- ],
+client.guest_badges.guest_badge_update(
+ email="alex@client.example",
+ host_user_id="3f1c0b2a-8d4e-4c91-9a7b-2e6f1d8c0a11",
+ visit_permission=False,
)
```
@@ -6604,7 +7529,7 @@ client.group.remove(
-
-**id:** `str` — Group ID
+**email:** `str` — Guest email. ASCII only.
@@ -6612,7 +7537,19 @@ client.group.remove(
-
-**members:** `typing.List[str]` — List of member IDs or email addresses to remove
+**visit_permission:** `bool` — Whether the guest may visit the host on the map.
+
+
+
+
+
+-
+
+**host_user_id:** `typing.Optional[str]`
+
+Host member. UUID or member email. Required when more than one
+host has granted this email. Optional for a unique grant, and
+for personal tokens (defaults to the token owner).
@@ -6632,8 +7569,7 @@ client.group.remove(
-## Groups
-client.groups.list() -> typing.List[GroupsListResponseItem]
+client.guest_badges.guest_badge_revoke(...) -> GuestBadgeRevokeResponse
-
@@ -6645,17 +7581,28 @@ client.group.remove(
-
-**Legacy:** Prefer [`/group.list`](https://developer.ro.am/docs/api/group-list) for new integrations.
+Revoke Guest Badge(s) for an email.
-Lists all public, non-archived groups in your home Roam.
+If only one host in the workspace has granted this email, `hostUserId` may
+be omitted. If several hosts have, pass `hostUserId` to pick which grant
+to revoke (`400` `missing_parameter` otherwise). Same-host alias rows are
+all revoked together.
-Unlike `/group.list`, this endpoint returns a **raw JSON array** (not the
-`{"ok": true, …}` envelope). It is the sole ok-envelope exception on `/v1`
-and remains only for existing callers.
+Returns `{ "revoked": true }` when a matching grant was found and deleted,
+or `{ "revoked": false }` when there was nothing to revoke (already gone,
+including after the host was archived — archiving a member deletes the
+badges they granted). Naming an archived host does not 404.
-**Access:** Organization only.
+Personal tokens can only revoke badges they issued. Naming another host is
+`403` `access_mode_not_supported`; omitting `hostUserId` when only another
+host granted the email is a no-op (`revoked: false`).
-**Required scope:** `group:read`
+**Access:** Organization and Personal.
+
+**Required scope:** `guest:write`. Personal Access Tokens use the
+`pat:guests:write` group.
+
+See [Guest Badges](https://developer.ro.am/docs/guides/guest-badges).
@@ -6678,7 +7625,9 @@ client = RoamClient(
environment=RoamClientEnvironment.DEFAULT,
)
-client.groups.list()
+client.guest_badges.guest_badge_revoke(
+ email="alex@client.example",
+)
```
@@ -6694,6 +7643,26 @@ client.groups.list()
-
+**email:** `str` — Guest email. ASCII only.
+
+
+
+
+
+-
+
+**host_user_id:** `typing.Optional[str]`
+
+Host member. UUID or member email. Required when more than one
+host has granted this email. Optional for a unique grant, and
+for personal tokens (defaults to the token owner).
+
+
+
+
+
+-
+
**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
@@ -6958,7 +7927,37 @@ v0-only — sending them here returns `400` / `Unrecognized event`.
Roam does not probe the destination URL when you subscribe — the
subscription is created immediately and the first delivery is a real event.
-See the [Webhooks overview](https://developer.ro.am/docs/webhooks/webhooks) for the full list of event names and their filters.
+Optional `filter` limits which occurrences are delivered. Which keys are
+valid depends on `event` — see that event's page and the
+[Event Filters](https://developer.ro.am/docs/webhooks/webhooks#event-filters) table. Omit
+`filter` to receive every occurrence. An empty object (`{}`) is rejected,
+as is a filter that does not apply to the event.
+
+**DMs only:**
+
+```json
+{
+ "url": "https://example.com/hooks/messages",
+ "event": "chat.message",
+ "filter": { "chatType": "dm" }
+}
+```
+
+**Grok Bot routine** (no ngrok). `destination.token` is write-only — list
+and subscribe responses echo `destination.type` only. See
+[Grok](https://developer.ro.am/docs/integrations/grok).
+
+```json
+{
+ "url": "https://api2.cursor.sh/automations/webhook/",
+ "event": "chat.message",
+ "filter": { "self": true },
+ "destination": {
+ "type": "grok_bot",
+ "token": ""
+ }
+}
+```
**Required scope:** `webhook:write`
@@ -6975,7 +7974,7 @@ See the [Webhooks overview](https://developer.ro.am/docs/webhooks/webhooks) for
-
```python
-from roamhq import RoamClient, WebhookSubscriptionFilter
+from roamhq import RoamClient
from roamhq.environment import RoamClientEnvironment
client = RoamClient(
@@ -6986,9 +7985,6 @@ client = RoamClient(
client.webhook.subscribe(
url="https://example.com/hooks/messages",
event="chat.message",
- filter=WebhookSubscriptionFilter(
- mention=True,
- ),
)
```
@@ -7022,6 +8018,11 @@ client.webhook.subscribe(
-
**filter:** `typing.Optional[WebhookSubscriptionFilter]`
+
+Optional event-specific filter. Which keys are valid depends on `event`
+(see the schema). Omit to receive every occurrence; `{}` and `null` are
+rejected rather than treated as "omitted". Example for DMs only:
+`{"chatType": "dm"}`.
@@ -7042,6 +8043,24 @@ frozen at your integration's default version. Unsupported values return
-
+**destination:** `typing.Optional[WebhookSubscriptionRequestDestination]`
+
+Optional delivery authentication. Omit for Standard Webhooks signed with
+the API client's `whsec_`. Set `type` to `grok_bot` to deliver to a
+[Grok Bot](https://developer.ro.am/docs/integrations/grok) webhook-routine URL: Roam signs with
+the routine's sender key (`Authorization: Bearer` plus
+`X-Grok-Signature`) or, if `token` is a `whsec_…` Standard Webhooks
+secret, uses that secret instead of the API client's. The token is
+write-only — subscribe and list responses echo `destination.type` only.
+Re-subscribe without this field leaves existing destination auth
+unchanged; send `"type": ""` to clear it.
+
+
+
+
+
+-
+
**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
diff --git a/src/roamhq/types/__init__.py b/src/roamhq/types/__init__.py
index a2b425a..3c64772 100644
--- a/src/roamhq/types/__init__.py
+++ b/src/roamhq/types/__init__.py
@@ -28,6 +28,7 @@
from .group_member import GroupMember
from .group_member_role import GroupMemberRole
from .group_type import GroupType
+ from .guest_badge import GuestBadge
from .lobby_booking import LobbyBooking
from .lobby_booking_host import LobbyBookingHost
from .lobby_booking_invitee import LobbyBookingInvitee
@@ -52,13 +53,17 @@
from .user_audit_log import UserAuditLog
from .user_audit_log_platform import UserAuditLogPlatform
from .user_status import UserStatus
+ from .user_status_bubble_response import UserStatusBubbleResponse
+ from .user_status_bubble_response_status_bubble import UserStatusBubbleResponseStatusBubble
from .user_type import UserType
- from .user_will_return import UserWillReturn
from .webhook import Webhook
+ from .webhook_destination import WebhookDestination
+ from .webhook_destination_type import WebhookDestinationType
from .webhook_event import WebhookEvent
from .webhook_subscription_filter import WebhookSubscriptionFilter
from .webhook_subscription_filter_chat_type import WebhookSubscriptionFilterChatType
from .webhook_subscription_filter_status import WebhookSubscriptionFilterStatus
+ from .will_return import WillReturn
_dynamic_imports: typing.Dict[str, str] = {
"ActionItem": ".action_item",
"Address": ".address",
@@ -80,6 +85,7 @@
"GroupMember": ".group_member",
"GroupMemberRole": ".group_member_role",
"GroupType": ".group_type",
+ "GuestBadge": ".guest_badge",
"LobbyBooking": ".lobby_booking",
"LobbyBookingHost": ".lobby_booking_host",
"LobbyBookingInvitee": ".lobby_booking_invitee",
@@ -104,13 +110,17 @@
"UserAuditLog": ".user_audit_log",
"UserAuditLogPlatform": ".user_audit_log_platform",
"UserStatus": ".user_status",
+ "UserStatusBubbleResponse": ".user_status_bubble_response",
+ "UserStatusBubbleResponseStatusBubble": ".user_status_bubble_response_status_bubble",
"UserType": ".user_type",
- "UserWillReturn": ".user_will_return",
"Webhook": ".webhook",
+ "WebhookDestination": ".webhook_destination",
+ "WebhookDestinationType": ".webhook_destination_type",
"WebhookEvent": ".webhook_event",
"WebhookSubscriptionFilter": ".webhook_subscription_filter",
"WebhookSubscriptionFilterChatType": ".webhook_subscription_filter_chat_type",
"WebhookSubscriptionFilterStatus": ".webhook_subscription_filter_status",
+ "WillReturn": ".will_return",
}
@@ -156,6 +166,7 @@ def __dir__():
"GroupMember",
"GroupMemberRole",
"GroupType",
+ "GuestBadge",
"LobbyBooking",
"LobbyBookingHost",
"LobbyBookingInvitee",
@@ -180,11 +191,15 @@ def __dir__():
"UserAuditLog",
"UserAuditLogPlatform",
"UserStatus",
+ "UserStatusBubbleResponse",
+ "UserStatusBubbleResponseStatusBubble",
"UserType",
- "UserWillReturn",
"Webhook",
+ "WebhookDestination",
+ "WebhookDestinationType",
"WebhookEvent",
"WebhookSubscriptionFilter",
"WebhookSubscriptionFilterChatType",
"WebhookSubscriptionFilterStatus",
+ "WillReturn",
]
diff --git a/src/roamhq/types/guest_badge.py b/src/roamhq/types/guest_badge.py
new file mode 100644
index 0000000..a9e2796
--- /dev/null
+++ b/src/roamhq/types/guest_badge.py
@@ -0,0 +1,68 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+import pydantic
+import typing_extensions
+from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from ..core.serialization import FieldMetadata
+
+
+class GuestBadge(UniversalBaseModel):
+ """
+ A Guest Badge granting a non-member email limited access hosted by a workspace member.
+ """
+
+ email: str = pydantic.Field()
+ """
+ The guest's email address (lowercase).
+ """
+
+ user_id: typing_extensions.Annotated[
+ typing.Optional[str],
+ FieldMetadata(alias="userId"),
+ pydantic.Field(
+ alias="userId",
+ description="The guest's chat address ID, when one has been provisioned. Omitted if\nthe address is not yet available.",
+ ),
+ ] = None
+ """
+ The guest's chat address ID, when one has been provisioned. Omitted if
+ the address is not yet available.
+ """
+
+ host_user_id: typing_extensions.Annotated[
+ str,
+ FieldMetadata(alias="hostUserId"),
+ pydantic.Field(
+ alias="hostUserId", description="The host member's user ID (always a UUID, even if you passed an email)."
+ ),
+ ]
+ """
+ The host member's user ID (always a UUID, even if you passed an email).
+ """
+
+ visit_permission: typing_extensions.Annotated[
+ bool,
+ FieldMetadata(alias="visitPermission"),
+ pydantic.Field(alias="visitPermission", description="Whether the guest may visit the host on the map."),
+ ]
+ """
+ Whether the guest may visit the host on the map.
+ """
+
+ acknowledged: bool = pydantic.Field()
+ """
+ Whether the guest has acknowledged the badge.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/roamhq/types/user.py b/src/roamhq/types/user.py
index 8bb1b12..42c0226 100644
--- a/src/roamhq/types/user.py
+++ b/src/roamhq/types/user.py
@@ -10,7 +10,7 @@
from ..core.serialization import FieldMetadata
from .user_status import UserStatus
from .user_type import UserType
-from .user_will_return import UserWillReturn
+from .will_return import WillReturn
class User(UniversalBaseModel):
@@ -92,15 +92,15 @@ class User(UniversalBaseModel):
"""
will_return: typing_extensions.Annotated[
- typing.Optional[UserWillReturn],
+ typing.Optional[WillReturn],
FieldMetadata(alias="willReturn"),
pydantic.Field(
alias="willReturn",
- 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.',
+ description="Present only when `expand=status` is requested, the `user:read.status` scope is granted, and the user has a future return time. Write it with [`user.status.set`](https://developer.ro.am/docs/api/user-status-set) / [`.clear`](https://developer.ro.am/docs/api/user-status-clear).",
),
] = None
"""
- 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.
+ Present only when `expand=status` is requested, the `user:read.status` scope is granted, and the user has a future return time. Write it with [`user.status.set`](https://developer.ro.am/docs/api/user-status-set) / [`.clear`](https://developer.ro.am/docs/api/user-status-clear).
"""
available: typing.Optional[bool] = pydantic.Field(default=None)
diff --git a/src/roamhq/types/user_activity_display.py b/src/roamhq/types/user_activity_display.py
index 202489b..3535c6d 100644
--- a/src/roamhq/types/user_activity_display.py
+++ b/src/roamhq/types/user_activity_display.py
@@ -17,8 +17,10 @@ class UserActivityDisplay(UniversalBaseModel):
emoji: str = pydantic.Field()
"""
- Badge shown on the user's seat. Required. At most 16 Unicode code
- points, so ZWJ sequences (family emoji, flags) stay valid.
+ Badge shown on the user's seat. Required. Must be a single emoji —
+ a text blurb or several emoji is `invalid_parameter`. ZWJ sequences
+ (family, flags, keycaps, skin tones) count as one. At most 16
+ Unicode code points (the storage cap).
"""
title: str = pydantic.Field()
diff --git a/src/roamhq/types/user_status_bubble_response.py b/src/roamhq/types/user_status_bubble_response.py
new file mode 100644
index 0000000..436864e
--- /dev/null
+++ b/src/roamhq/types/user_status_bubble_response.py
@@ -0,0 +1,42 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+import pydantic
+import typing_extensions
+from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from ..core.serialization import FieldMetadata
+from .user_status_bubble_response_status_bubble import UserStatusBubbleResponseStatusBubble
+
+
+class UserStatusBubbleResponse(UniversalBaseModel):
+ user_id: typing_extensions.Annotated[
+ str,
+ FieldMetadata(alias="userId"),
+ pydantic.Field(alias="userId", description="Canonical UUID of the target user."),
+ ]
+ """
+ Canonical UUID of the target user.
+ """
+
+ status_bubble: typing_extensions.Annotated[
+ typing.Optional[UserStatusBubbleResponseStatusBubble],
+ FieldMetadata(alias="statusBubble"),
+ pydantic.Field(
+ alias="statusBubble", description="The live thought bubble, or null when none is set or it has expired."
+ ),
+ ] = None
+ """
+ The live thought bubble, or null when none is set or it has expired.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/roamhq/types/user_status_bubble_response_status_bubble.py b/src/roamhq/types/user_status_bubble_response_status_bubble.py
new file mode 100644
index 0000000..6717ca4
--- /dev/null
+++ b/src/roamhq/types/user_status_bubble_response_status_bubble.py
@@ -0,0 +1,42 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import datetime as dt
+import typing
+
+import pydantic
+import typing_extensions
+from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from ..core.serialization import FieldMetadata
+
+
+class UserStatusBubbleResponseStatusBubble(UniversalBaseModel):
+ """
+ The live thought bubble, or null when none is set or it has expired.
+ """
+
+ text: str = pydantic.Field()
+ """
+ Trimmed text, limited to 20 Unicode code points.
+ """
+
+ expires_at: typing_extensions.Annotated[
+ dt.datetime,
+ FieldMetadata(alias="expiresAt"),
+ pydantic.Field(
+ alias="expiresAt", description="Server-stamped expiration in RFC3339 format with millisecond precision."
+ ),
+ ]
+ """
+ Server-stamped expiration in RFC3339 format with millisecond precision.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/roamhq/types/webhook.py b/src/roamhq/types/webhook.py
index 4695410..61fd7c8 100644
--- a/src/roamhq/types/webhook.py
+++ b/src/roamhq/types/webhook.py
@@ -9,6 +9,7 @@
import typing_extensions
from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
from ..core.serialization import FieldMetadata
+from .webhook_destination import WebhookDestination
from .webhook_event import WebhookEvent
from .webhook_subscription_filter import WebhookSubscriptionFilter
@@ -34,6 +35,13 @@ class Webhook(UniversalBaseModel):
Event-specific filter applied to the subscription.
"""
+ destination: typing.Optional[WebhookDestination] = pydantic.Field(default=None)
+ """
+ Delivery authentication. Omitted for Standard Webhooks (the default).
+ `type` is `grok_bot` when the subscription was created with
+ `destination.type=grok_bot`. The sender key is never returned.
+ """
+
dynamic: bool = pydantic.Field()
"""
`true` if the subscription was created via `/webhook.subscribe`.
diff --git a/src/roamhq/types/webhook_destination.py b/src/roamhq/types/webhook_destination.py
new file mode 100644
index 0000000..ad26eb8
--- /dev/null
+++ b/src/roamhq/types/webhook_destination.py
@@ -0,0 +1,28 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+import pydantic
+from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from .webhook_destination_type import WebhookDestinationType
+
+
+class WebhookDestination(UniversalBaseModel):
+ """
+ Delivery authentication. Omitted for Standard Webhooks (the default).
+ `type` is `grok_bot` when the subscription was created with
+ `destination.type=grok_bot`. The sender key is never returned.
+ """
+
+ type: typing.Optional[WebhookDestinationType] = None
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/roamhq/types/webhook_destination_type.py b/src/roamhq/types/webhook_destination_type.py
new file mode 100644
index 0000000..60d6472
--- /dev/null
+++ b/src/roamhq/types/webhook_destination_type.py
@@ -0,0 +1,7 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+WebhookDestinationType = typing.Union[typing.Literal["grok_bot"], typing.Any]
diff --git a/src/roamhq/types/webhook_subscription_filter.py b/src/roamhq/types/webhook_subscription_filter.py
index e47522c..f6fd231 100644
--- a/src/roamhq/types/webhook_subscription_filter.py
+++ b/src/roamhq/types/webhook_subscription_filter.py
@@ -14,7 +14,20 @@
class WebhookSubscriptionFilter(UniversalBaseModel):
"""
- Event-specific filter to limit webhook notifications. Different properties apply to different events.
+ Event-specific filter passed as `filter` on `/webhook.subscribe`. Omit the
+ field to receive every occurrence of the event. A present but empty filter is
+ rejected — both `{}` and `null`.
+
+ Which properties apply depends on `event`:
+
+ - `chat.message`: `chatType` (`dm` or `group`) and/or `mention`
+ - `chat.reaction`: `names`
+ - `meeting.ended`: `hasVideo` (`true` only)
+ - `onair.event.created` / `updated` / `canceled` and `onair.guest.added`: `eventId`
+ - `onair.guest.rsvp`: `eventId` and/or `status`
+ - all other events: do not accept a filter
+
+ Example — DMs only: `{"chatType": "dm"}`.
"""
chat_type: typing_extensions.Annotated[
@@ -22,16 +35,21 @@ class WebhookSubscriptionFilter(UniversalBaseModel):
FieldMetadata(alias="chatType"),
pydantic.Field(
alias="chatType",
- description="For `chat.message`: restrict to direct messages (`dm`) or group messages (`group`).",
+ description="For `chat.message`: restrict to direct messages (`dm`, 1:1 and\nmulti-person) or group messages (`group`, including meeting channels).\nSame vocabulary as `data.chatType` on the delivered payload.",
),
] = None
"""
- For `chat.message`: restrict to direct messages (`dm`) or group messages (`group`).
+ For `chat.message`: restrict to direct messages (`dm`, 1:1 and
+ multi-person) or group messages (`group`, including meeting channels).
+ Same vocabulary as `data.chatType` on the delivered payload.
"""
mention: typing.Optional[bool] = pydantic.Field(default=None)
"""
- For `chat.message`: restrict to messages that @mention your app.
+ For `chat.message`: restrict to messages that @mention your app. Only
+ `true` constrains anything, so `{"mention": false}` on its own is
+ rejected like `{}`; alongside another key (`{"chatType": "dm",
+ "mention": false}`) it is accepted and ignored.
"""
names: typing.Optional[typing.List[str]] = pydantic.Field(default=None)
diff --git a/src/roamhq/types/user_will_return.py b/src/roamhq/types/will_return.py
similarity index 55%
rename from src/roamhq/types/user_will_return.py
rename to src/roamhq/types/will_return.py
index 24bc62b..8506960 100644
--- a/src/roamhq/types/user_will_return.py
+++ b/src/roamhq/types/will_return.py
@@ -11,9 +11,15 @@
from ..core.serialization import FieldMetadata
-class UserWillReturn(UniversalBaseModel):
+class WillReturn(UniversalBaseModel):
"""
- 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.
+ Out-of-office / "Will Return" status. 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.
+
+ On `user.info` / `user.list` (with `expand=status`) and `user.status.update`
+ webhooks, elapsed return times are omitted. On `user.status.set` the written
+ value is always returned.
"""
return_time: typing_extensions.Annotated[
@@ -27,7 +33,8 @@ class UserWillReturn(UniversalBaseModel):
reason: typing.Optional[str] = pydantic.Field(default=None)
"""
- Optional absence message (e.g. "On Vacation").
+ Optional absence message (for example "On Vacation" or "Out to lunch").
+ At most 128 Unicode code points.
"""
out_of_roam: typing_extensions.Annotated[
@@ -35,11 +42,16 @@ class UserWillReturn(UniversalBaseModel):
FieldMetadata(alias="outOfRoam"),
pydantic.Field(
alias="outOfRoam",
- description="When true, multi-day Out of Roam that persists across check-ins. When false or omitted, same-day Will Return Today.",
+ description="When true, multi-day Out of Roam that persists across check-ins. When\nfalse or omitted on a **read**, same-day Will Return Today.\n\n`user.status.set` defaults omitted `outOfRoam` to **true**. Will Return\nToday must send `outOfRoam: false` explicitly, and `returnTime` must be\nless than 10 hours from now.",
),
] = None
"""
- When true, multi-day Out of Roam that persists across check-ins. When false or omitted, same-day Will Return Today.
+ When true, multi-day Out of Roam that persists across check-ins. When
+ false or omitted on a **read**, same-day Will Return Today.
+
+ `user.status.set` defaults omitted `outOfRoam` to **true**. Will Return
+ Today must send `outOfRoam: false` explicitly, and `returnTime` must be
+ less than 10 hours from now.
"""
if IS_PYDANTIC_V2:
diff --git a/src/roamhq/user/client.py b/src/roamhq/user/client.py
index 2570dbc..66a2bba 100644
--- a/src/roamhq/user/client.py
+++ b/src/roamhq/user/client.py
@@ -78,7 +78,7 @@ def list(
Opaque directory cursor from a previous response's `nextCursor`. Cannot be combined with `ids`.
expand : typing.Optional[str]
- Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set.
+ Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set. Write that field with `user.status.set` / `.clear`.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -141,7 +141,7 @@ def info(
The user's email address. Mutually exclusive with `id`. Requires `user:read.email` scope.
expand : typing.Optional[str]
- 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.
+ 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. Write that field with `user.status.set` / `.clear`.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -232,7 +232,7 @@ async def list(
Opaque directory cursor from a previous response's `nextCursor`. Cannot be combined with `ids`.
expand : typing.Optional[str]
- Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set.
+ Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set. Write that field with `user.status.set` / `.clear`.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -303,7 +303,7 @@ async def info(
The user's email address. Mutually exclusive with `id`. Requires `user:read.email` scope.
expand : typing.Optional[str]
- 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.
+ 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. Write that field with `user.status.set` / `.clear`.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
diff --git a/src/roamhq/user/raw_client.py b/src/roamhq/user/raw_client.py
index 3c7d684..21da0eb 100644
--- a/src/roamhq/user/raw_client.py
+++ b/src/roamhq/user/raw_client.py
@@ -80,7 +80,7 @@ def list(
Opaque directory cursor from a previous response's `nextCursor`. Cannot be combined with `ids`.
expand : typing.Optional[str]
- Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set.
+ Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set. Write that field with `user.status.set` / `.clear`.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -214,7 +214,7 @@ def info(
The user's email address. Mutually exclusive with `id`. Requires `user:read.email` scope.
expand : typing.Optional[str]
- 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.
+ 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. Write that field with `user.status.set` / `.clear`.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -387,7 +387,7 @@ async def list(
Opaque directory cursor from a previous response's `nextCursor`. Cannot be combined with `ids`.
expand : typing.Optional[str]
- Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set.
+ Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set. Write that field with `user.status.set` / `.clear`.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -521,7 +521,7 @@ async def info(
The user's email address. Mutually exclusive with `id`. Requires `user:read.email` scope.
expand : typing.Optional[str]
- 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.
+ 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. Write that field with `user.status.set` / `.clear`.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
diff --git a/src/roamhq/users/__init__.py b/src/roamhq/users/__init__.py
index 5fbb2a7..bdb06a2 100644
--- a/src/roamhq/users/__init__.py
+++ b/src/roamhq/users/__init__.py
@@ -8,8 +8,18 @@
from importlib import import_module
if typing.TYPE_CHECKING:
- from .types import UserActivityListResponse
-_dynamic_imports: typing.Dict[str, str] = {"UserActivityListResponse": ".types"}
+ from .types import (
+ UserActivityListResponse,
+ UserStatusSetRequestWillReturn,
+ UserStatusSetResponse,
+ UserStatusSetResponseStatus,
+ )
+_dynamic_imports: typing.Dict[str, str] = {
+ "UserActivityListResponse": ".types",
+ "UserStatusSetRequestWillReturn": ".types",
+ "UserStatusSetResponse": ".types",
+ "UserStatusSetResponseStatus": ".types",
+}
def __getattr__(attr_name: str) -> typing.Any:
@@ -33,4 +43,9 @@ def __dir__():
return sorted(lazy_attrs)
-__all__ = ["UserActivityListResponse"]
+__all__ = [
+ "UserActivityListResponse",
+ "UserStatusSetRequestWillReturn",
+ "UserStatusSetResponse",
+ "UserStatusSetResponseStatus",
+]
diff --git a/src/roamhq/users/client.py b/src/roamhq/users/client.py
index b39356a..bdcc979 100644
--- a/src/roamhq/users/client.py
+++ b/src/roamhq/users/client.py
@@ -9,8 +9,11 @@
from ..core.request_options import RequestOptions
from ..types.user_activity import UserActivity
from ..types.user_activity_display import UserActivityDisplay
+from ..types.user_status_bubble_response import UserStatusBubbleResponse
from .raw_client import AsyncRawUsersClient, RawUsersClient
from .types.user_activity_list_response import UserActivityListResponse
+from .types.user_status_set_request_will_return import UserStatusSetRequestWillReturn
+from .types.user_status_set_response import UserStatusSetResponse
# this is used as the default value for optional parameters
OMIT = typing.cast(typing.Any, ...)
@@ -31,6 +34,287 @@ def with_raw_response(self) -> RawUsersClient:
"""
return self._raw_client
+ def user_status_set(
+ self,
+ *,
+ user_id: str,
+ will_return: UserStatusSetRequestWillReturn,
+ status: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> UserStatusSetResponse:
+ """
+ Record an absence on a workspace member — the same Will Return Today /
+ Out of Roam field the desktop client writes, already readable via
+ [`user.info?expand=status`](https://developer.ro.am/docs/api/user-info) and
+ [`user.status.update`](https://developer.ro.am/docs/webhooks/user-status-update).
+
+ This is **not** [external activity](https://developer.ro.am/docs/guides/user-activity). Use
+ `user.status.set` for HR absences (sick leave, vacation, parental leave,
+ public holidays). Use `user.activity.set` for a short-lived on-map glow
+ / emoji (phone call, browser meeting).
+
+ `willReturn` is last-writer-wins with the desktop client. Setting it
+ does **not** check the user out, does **not** enable Do Not Disturb, and
+ does **not** accept a `status` enum (`checkedIn` / `checkedOut` stay
+ read-only).
+
+ `outOfRoam` defaults to `true` (persistent Out of Roam, up to 2 years).
+ Pass `outOfRoam: false` for same-day Will Return Today (`returnTime`
+ must be less than 10 hours from now).
+
+ Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ ASCII email (same convention as `group.create` members). Third-party
+ systems that only have an email do not need a UUID lookup first.
+
+ See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for the two
+ modes, persistence across check-in, and an HRIS example.
+
+ **Access:** Organization and Personal. Organization tokens may target
+ any active member in the workspace. Personal tokens (OAuth or PAT) may
+ target only the token owner.
+
+ **Required scope:** `user:write.status`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+ Reading the field back via `user.info` still needs `user:read.status`.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user. Does not require
+ `user:read.email` — email is an identifier, not a
+ disclosure.
+
+ will_return : UserStatusSetRequestWillReturn
+ Absence to write. Required. Replaces any existing Will
+ Return / Out of Roam on this user.
+
+ status : typing.Optional[str]
+ Rejected. Check-in status is read-only; absences go in
+ `willReturn`. Sending this field returns `400`
+ `invalid_arguments`.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ UserStatusSetResponse
+ Absence saved. `willReturn` echoes the written value (including
+ defaulted `outOfRoam`). `status` is the user's current check-in
+ (`checkedIn` / `checkedOut`) and is unchanged by this call. `userId`
+ is the canonical UUID.
+
+ Examples
+ --------
+ import datetime
+
+ from roamhq import RoamClient
+ from roamhq.users import UserStatusSetRequestWillReturn
+
+ client = RoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+ client.users.user_status_set(
+ user_id="ada@example.com",
+ will_return=UserStatusSetRequestWillReturn(
+ return_time=datetime.datetime.fromisoformat(
+ "2026-09-22 09:00:00+00:00",
+ ),
+ reason="On vacation",
+ out_of_roam=True,
+ ),
+ )
+ """
+ _response = self._raw_client.user_status_set(
+ user_id=user_id, will_return=will_return, status=status, request_options=request_options
+ )
+ return _response.data
+
+ def user_status_clear(self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None) -> None:
+ """
+ Remove the Will Return / Out of Roam previously written with
+ [`user.status.set`](https://developer.ro.am/docs/api/user-status-set) or the desktop client.
+ Clears both same-day Will Return Today and persistent Out of Roam.
+
+ Clearing when nothing is set still returns **204**. This call does
+ **not** change check-in status.
+
+ See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for
+ persistence, check-in interaction, and the HRIS lifecycle.
+
+ **Access:** Organization and Personal. Organization tokens may target
+ any active member in the workspace. Personal tokens (OAuth or PAT) may
+ target only the token owner.
+
+ **Required scope:** `user:write.status`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ None
+
+ Examples
+ --------
+ from roamhq import RoamClient
+
+ client = RoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+ client.users.user_status_clear(
+ user_id="ada@example.com",
+ )
+ """
+ _response = self._raw_client.user_status_clear(user_id=user_id, request_options=request_options)
+ return _response.data
+
+ def user_status_bubble_set(
+ self,
+ *,
+ user_id: str,
+ text: str,
+ ttl_seconds: typing.Optional[int] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> UserStatusBubbleResponse:
+ """
+ Replaces the shared UI/API thought bubble. Text is trimmed and limited to 1–20 Unicode code points. Omitted or null ttlSeconds defaults to 86400; integers from 300 through 86400 are accepted. Durations below 5 minutes or above 24 hours are rejected. Automatic map removal can take up to about a minute after expiration. Repeating set refreshes expiration. Sending expiresAt is rejected.
+
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+ **Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+ text : str
+ Text to trim and store. Must contain 1–20 Unicode code points after trimming. Blank text is rejected.
+
+ ttl_seconds : typing.Optional[int]
+ Optional duration. Omit or pass null for 24 hours. Durations outside 300–86400 seconds are rejected.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ UserStatusBubbleResponse
+ Current bubble and canonical user identifier.
+
+ Examples
+ --------
+ from roamhq import RoamClient
+
+ client = RoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+ client.users.user_status_bubble_set(
+ user_id="ada@example.com",
+ text="At lunch 🍎",
+ ttl_seconds=3600,
+ )
+ """
+ _response = self._raw_client.user_status_bubble_set(
+ user_id=user_id, text=text, ttl_seconds=ttl_seconds, request_options=request_options
+ )
+ return _response.data
+
+ def user_status_bubble_get(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> UserStatusBubbleResponse:
+ """
+ Returns the shared UI/API thought bubble, or null when no live bubble exists. Expired bubbles are omitted before storage cleanup runs.
+
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+ **Required scope:** `user:read.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ UserStatusBubbleResponse
+ Current bubble and canonical user identifier.
+
+ Examples
+ --------
+ from roamhq import RoamClient
+
+ client = RoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+ client.users.user_status_bubble_get(
+ user_id="userId",
+ )
+ """
+ _response = self._raw_client.user_status_bubble_get(user_id=user_id, request_options=request_options)
+ return _response.data
+
+ def user_status_bubble_clear(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> None:
+ """
+ Clears the shared thought bubble, including a bubble written by the UI or another integration. Repeating clear is a successful no-op. This does not clear Will Return or external activities.
+
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+ **Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ None
+
+ Examples
+ --------
+ from roamhq import RoamClient
+
+ client = RoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+ client.users.user_status_bubble_clear(
+ user_id="ada@example.com",
+ )
+ """
+ _response = self._raw_client.user_status_bubble_clear(user_id=user_id, request_options=request_options)
+ return _response.data
+
def user_activity_set(
self,
*,
@@ -62,6 +346,10 @@ def user_activity_set(
See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
TTL, stacking, and where the indicator appears on the map.
+ Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ ASCII email (same convention as `group.create` members). Third-party
+ systems that only have an email do not need a UUID lookup first.
+
**Access:** Organization and Personal. Organization tokens may target
any user in the workspace. Personal tokens (OAuth or PAT) may target
only the token owner.
@@ -72,8 +360,11 @@ def user_activity_set(
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only
- pass their own user.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user. Does not require
+ `user:read.email` — email is an identifier, not a
+ disclosure.
external_id : str
Caller-chosen session id, unique per integration and user.
@@ -124,7 +415,7 @@ def user_activity_set(
token="YOUR_TOKEN",
)
client.users.user_activity_set(
- user_id="0cc74785-e31e-4403-aa5e-0cc7c1897e66",
+ user_id="ada@example.com",
external_id="justcall:call:CA123",
display=UserActivityDisplay(
emoji="📞",
@@ -173,8 +464,9 @@ def user_activity_clear(
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only
- pass their own user.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user.
external_id : str
The `externalId` previously passed to `user.activity.set`.
@@ -195,7 +487,7 @@ def user_activity_clear(
token="YOUR_TOKEN",
)
client.users.user_activity_clear(
- user_id="0cc74785-e31e-4403-aa5e-0cc7c1897e66",
+ user_id="ada@example.com",
external_id="justcall:call:CA123",
)
"""
@@ -230,8 +522,9 @@ def user_activity_list(
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only pass
- their own user.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal tokens
+ may only pass their own user.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -377,6 +670,326 @@ def with_raw_response(self) -> AsyncRawUsersClient:
"""
return self._raw_client
+ async def user_status_set(
+ self,
+ *,
+ user_id: str,
+ will_return: UserStatusSetRequestWillReturn,
+ status: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> UserStatusSetResponse:
+ """
+ Record an absence on a workspace member — the same Will Return Today /
+ Out of Roam field the desktop client writes, already readable via
+ [`user.info?expand=status`](https://developer.ro.am/docs/api/user-info) and
+ [`user.status.update`](https://developer.ro.am/docs/webhooks/user-status-update).
+
+ This is **not** [external activity](https://developer.ro.am/docs/guides/user-activity). Use
+ `user.status.set` for HR absences (sick leave, vacation, parental leave,
+ public holidays). Use `user.activity.set` for a short-lived on-map glow
+ / emoji (phone call, browser meeting).
+
+ `willReturn` is last-writer-wins with the desktop client. Setting it
+ does **not** check the user out, does **not** enable Do Not Disturb, and
+ does **not** accept a `status` enum (`checkedIn` / `checkedOut` stay
+ read-only).
+
+ `outOfRoam` defaults to `true` (persistent Out of Roam, up to 2 years).
+ Pass `outOfRoam: false` for same-day Will Return Today (`returnTime`
+ must be less than 10 hours from now).
+
+ Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ ASCII email (same convention as `group.create` members). Third-party
+ systems that only have an email do not need a UUID lookup first.
+
+ See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for the two
+ modes, persistence across check-in, and an HRIS example.
+
+ **Access:** Organization and Personal. Organization tokens may target
+ any active member in the workspace. Personal tokens (OAuth or PAT) may
+ target only the token owner.
+
+ **Required scope:** `user:write.status`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+ Reading the field back via `user.info` still needs `user:read.status`.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user. Does not require
+ `user:read.email` — email is an identifier, not a
+ disclosure.
+
+ will_return : UserStatusSetRequestWillReturn
+ Absence to write. Required. Replaces any existing Will
+ Return / Out of Roam on this user.
+
+ status : typing.Optional[str]
+ Rejected. Check-in status is read-only; absences go in
+ `willReturn`. Sending this field returns `400`
+ `invalid_arguments`.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ UserStatusSetResponse
+ Absence saved. `willReturn` echoes the written value (including
+ defaulted `outOfRoam`). `status` is the user's current check-in
+ (`checkedIn` / `checkedOut`) and is unchanged by this call. `userId`
+ is the canonical UUID.
+
+ Examples
+ --------
+ import asyncio
+ import datetime
+
+ from roamhq import AsyncRoamClient
+ from roamhq.users import UserStatusSetRequestWillReturn
+
+ client = AsyncRoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.users.user_status_set(
+ user_id="ada@example.com",
+ will_return=UserStatusSetRequestWillReturn(
+ return_time=datetime.datetime.fromisoformat(
+ "2026-09-22 09:00:00+00:00",
+ ),
+ reason="On vacation",
+ out_of_roam=True,
+ ),
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.user_status_set(
+ user_id=user_id, will_return=will_return, status=status, request_options=request_options
+ )
+ return _response.data
+
+ async def user_status_clear(self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None) -> None:
+ """
+ Remove the Will Return / Out of Roam previously written with
+ [`user.status.set`](https://developer.ro.am/docs/api/user-status-set) or the desktop client.
+ Clears both same-day Will Return Today and persistent Out of Roam.
+
+ Clearing when nothing is set still returns **204**. This call does
+ **not** change check-in status.
+
+ See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for
+ persistence, check-in interaction, and the HRIS lifecycle.
+
+ **Access:** Organization and Personal. Organization tokens may target
+ any active member in the workspace. Personal tokens (OAuth or PAT) may
+ target only the token owner.
+
+ **Required scope:** `user:write.status`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ None
+
+ Examples
+ --------
+ import asyncio
+
+ from roamhq import AsyncRoamClient
+
+ client = AsyncRoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.users.user_status_clear(
+ user_id="ada@example.com",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.user_status_clear(user_id=user_id, request_options=request_options)
+ return _response.data
+
+ async def user_status_bubble_set(
+ self,
+ *,
+ user_id: str,
+ text: str,
+ ttl_seconds: typing.Optional[int] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> UserStatusBubbleResponse:
+ """
+ Replaces the shared UI/API thought bubble. Text is trimmed and limited to 1–20 Unicode code points. Omitted or null ttlSeconds defaults to 86400; integers from 300 through 86400 are accepted. Durations below 5 minutes or above 24 hours are rejected. Automatic map removal can take up to about a minute after expiration. Repeating set refreshes expiration. Sending expiresAt is rejected.
+
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+ **Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+ text : str
+ Text to trim and store. Must contain 1–20 Unicode code points after trimming. Blank text is rejected.
+
+ ttl_seconds : typing.Optional[int]
+ Optional duration. Omit or pass null for 24 hours. Durations outside 300–86400 seconds are rejected.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ UserStatusBubbleResponse
+ Current bubble and canonical user identifier.
+
+ Examples
+ --------
+ import asyncio
+
+ from roamhq import AsyncRoamClient
+
+ client = AsyncRoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.users.user_status_bubble_set(
+ user_id="ada@example.com",
+ text="At lunch 🍎",
+ ttl_seconds=3600,
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.user_status_bubble_set(
+ user_id=user_id, text=text, ttl_seconds=ttl_seconds, request_options=request_options
+ )
+ return _response.data
+
+ async def user_status_bubble_get(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> UserStatusBubbleResponse:
+ """
+ Returns the shared UI/API thought bubble, or null when no live bubble exists. Expired bubbles are omitted before storage cleanup runs.
+
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+ **Required scope:** `user:read.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ UserStatusBubbleResponse
+ Current bubble and canonical user identifier.
+
+ Examples
+ --------
+ import asyncio
+
+ from roamhq import AsyncRoamClient
+
+ client = AsyncRoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.users.user_status_bubble_get(
+ user_id="userId",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.user_status_bubble_get(user_id=user_id, request_options=request_options)
+ return _response.data
+
+ async def user_status_bubble_clear(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> None:
+ """
+ Clears the shared thought bubble, including a bubble written by the UI or another integration. Repeating clear is a successful no-op. This does not clear Will Return or external activities.
+
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+ **Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ None
+
+ Examples
+ --------
+ import asyncio
+
+ from roamhq import AsyncRoamClient
+
+ client = AsyncRoamClient(
+ roam_version="YOUR_ROAM_VERSION",
+ token="YOUR_TOKEN",
+ )
+
+
+ async def main() -> None:
+ await client.users.user_status_bubble_clear(
+ user_id="ada@example.com",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.user_status_bubble_clear(user_id=user_id, request_options=request_options)
+ return _response.data
+
async def user_activity_set(
self,
*,
@@ -408,6 +1021,10 @@ async def user_activity_set(
See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
TTL, stacking, and where the indicator appears on the map.
+ Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ ASCII email (same convention as `group.create` members). Third-party
+ systems that only have an email do not need a UUID lookup first.
+
**Access:** Organization and Personal. Organization tokens may target
any user in the workspace. Personal tokens (OAuth or PAT) may target
only the token owner.
@@ -418,8 +1035,11 @@ async def user_activity_set(
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only
- pass their own user.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user. Does not require
+ `user:read.email` — email is an identifier, not a
+ disclosure.
external_id : str
Caller-chosen session id, unique per integration and user.
@@ -475,7 +1095,7 @@ async def user_activity_set(
async def main() -> None:
await client.users.user_activity_set(
- user_id="0cc74785-e31e-4403-aa5e-0cc7c1897e66",
+ user_id="ada@example.com",
external_id="justcall:call:CA123",
display=UserActivityDisplay(
emoji="📞",
@@ -527,8 +1147,9 @@ async def user_activity_clear(
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only
- pass their own user.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user.
external_id : str
The `externalId` previously passed to `user.activity.set`.
@@ -554,7 +1175,7 @@ async def user_activity_clear(
async def main() -> None:
await client.users.user_activity_clear(
- user_id="0cc74785-e31e-4403-aa5e-0cc7c1897e66",
+ user_id="ada@example.com",
external_id="justcall:call:CA123",
)
@@ -592,8 +1213,9 @@ async def user_activity_list(
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only pass
- their own user.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal tokens
+ may only pass their own user.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
diff --git a/src/roamhq/users/raw_client.py b/src/roamhq/users/raw_client.py
index e62b605..bd1531e 100644
--- a/src/roamhq/users/raw_client.py
+++ b/src/roamhq/users/raw_client.py
@@ -23,7 +23,10 @@
from ..types.error import Error
from ..types.user_activity import UserActivity
from ..types.user_activity_display import UserActivityDisplay
+from ..types.user_status_bubble_response import UserStatusBubbleResponse
from .types.user_activity_list_response import UserActivityListResponse
+from .types.user_status_set_request_will_return import UserStatusSetRequestWillReturn
+from .types.user_status_set_response import UserStatusSetResponse
from pydantic import ValidationError
# this is used as the default value for optional parameters
@@ -34,103 +37,1623 @@ class RawUsersClient:
def __init__(self, *, client_wrapper: SyncClientWrapper):
self._client_wrapper = client_wrapper
+ def user_status_set(
+ self,
+ *,
+ user_id: str,
+ will_return: UserStatusSetRequestWillReturn,
+ status: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[UserStatusSetResponse]:
+ """
+ Record an absence on a workspace member — the same Will Return Today /
+ Out of Roam field the desktop client writes, already readable via
+ [`user.info?expand=status`](https://developer.ro.am/docs/api/user-info) and
+ [`user.status.update`](https://developer.ro.am/docs/webhooks/user-status-update).
+
+ This is **not** [external activity](https://developer.ro.am/docs/guides/user-activity). Use
+ `user.status.set` for HR absences (sick leave, vacation, parental leave,
+ public holidays). Use `user.activity.set` for a short-lived on-map glow
+ / emoji (phone call, browser meeting).
+
+ `willReturn` is last-writer-wins with the desktop client. Setting it
+ does **not** check the user out, does **not** enable Do Not Disturb, and
+ does **not** accept a `status` enum (`checkedIn` / `checkedOut` stay
+ read-only).
+
+ `outOfRoam` defaults to `true` (persistent Out of Roam, up to 2 years).
+ Pass `outOfRoam: false` for same-day Will Return Today (`returnTime`
+ must be less than 10 hours from now).
+
+ Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ ASCII email (same convention as `group.create` members). Third-party
+ systems that only have an email do not need a UUID lookup first.
+
+ See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for the two
+ modes, persistence across check-in, and an HRIS example.
+
+ **Access:** Organization and Personal. Organization tokens may target
+ any active member in the workspace. Personal tokens (OAuth or PAT) may
+ target only the token owner.
+
+ **Required scope:** `user:write.status`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+ Reading the field back via `user.info` still needs `user:read.status`.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user. Does not require
+ `user:read.email` — email is an identifier, not a
+ disclosure.
+
+ will_return : UserStatusSetRequestWillReturn
+ Absence to write. Required. Replaces any existing Will
+ Return / Out of Roam on this user.
+
+ status : typing.Optional[str]
+ Rejected. Check-in status is read-only; absences go in
+ `willReturn`. Sending this field returns `400`
+ `invalid_arguments`.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[UserStatusSetResponse]
+ Absence saved. `willReturn` echoes the written value (including
+ defaulted `outOfRoam`). `status` is the user's current check-in
+ (`checkedIn` / `checkedOut`) and is unchanged by this call. `userId`
+ is the canonical UUID.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "user.status.set",
+ method="POST",
+ json={
+ "userId": user_id,
+ "willReturn": convert_and_respect_annotation_metadata(
+ object_=will_return, annotation=UserStatusSetRequestWillReturn, direction="write"
+ ),
+ "status": status,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ UserStatusSetResponse,
+ parse_obj_as(
+ type_=UserStatusSetResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def user_status_clear(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[None]:
+ """
+ Remove the Will Return / Out of Roam previously written with
+ [`user.status.set`](https://developer.ro.am/docs/api/user-status-set) or the desktop client.
+ Clears both same-day Will Return Today and persistent Out of Roam.
+
+ Clearing when nothing is set still returns **204**. This call does
+ **not** change check-in status.
+
+ See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for
+ persistence, check-in interaction, and the HRIS lifecycle.
+
+ **Access:** Organization and Personal. Organization tokens may target
+ any active member in the workspace. Personal tokens (OAuth or PAT) may
+ target only the token owner.
+
+ **Required scope:** `user:write.status`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[None]
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "user.status.clear",
+ method="POST",
+ json={
+ "userId": user_id,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ return HttpResponse(response=_response, data=None)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def user_status_bubble_set(
+ self,
+ *,
+ user_id: str,
+ text: str,
+ ttl_seconds: typing.Optional[int] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[UserStatusBubbleResponse]:
+ """
+ Replaces the shared UI/API thought bubble. Text is trimmed and limited to 1–20 Unicode code points. Omitted or null ttlSeconds defaults to 86400; integers from 300 through 86400 are accepted. Durations below 5 minutes or above 24 hours are rejected. Automatic map removal can take up to about a minute after expiration. Repeating set refreshes expiration. Sending expiresAt is rejected.
+
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+ **Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+ text : str
+ Text to trim and store. Must contain 1–20 Unicode code points after trimming. Blank text is rejected.
+
+ ttl_seconds : typing.Optional[int]
+ Optional duration. Omit or pass null for 24 hours. Durations outside 300–86400 seconds are rejected.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[UserStatusBubbleResponse]
+ Current bubble and canonical user identifier.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "user.statusBubble.set",
+ method="POST",
+ json={
+ "userId": user_id,
+ "text": text,
+ "ttlSeconds": ttl_seconds,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ UserStatusBubbleResponse,
+ parse_obj_as(
+ type_=UserStatusBubbleResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def user_status_bubble_get(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[UserStatusBubbleResponse]:
+ """
+ Returns the shared UI/API thought bubble, or null when no live bubble exists. Expired bubbles are omitted before storage cleanup runs.
+
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+ **Required scope:** `user:read.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[UserStatusBubbleResponse]
+ Current bubble and canonical user identifier.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "user.statusBubble.get",
+ method="GET",
+ params={
+ "userId": user_id,
+ },
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ UserStatusBubbleResponse,
+ parse_obj_as(
+ type_=UserStatusBubbleResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def user_status_bubble_clear(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[None]:
+ """
+ Clears the shared thought bubble, including a bubble written by the UI or another integration. Repeating clear is a successful no-op. This does not clear Will Return or external activities.
+
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
+
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
+
+ **Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[None]
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "user.statusBubble.clear",
+ method="POST",
+ json={
+ "userId": user_id,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ return HttpResponse(response=_response, data=None)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
def user_activity_set(
self,
*,
user_id: str,
- external_id: str,
- display: UserActivityDisplay,
- ttl_seconds: typing.Optional[int] = OMIT,
- expires_at: typing.Optional[dt.datetime] = OMIT,
- started_at: typing.Optional[dt.datetime] = OMIT,
- dnd: typing.Optional[bool] = OMIT,
+ external_id: str,
+ display: UserActivityDisplay,
+ ttl_seconds: typing.Optional[int] = OMIT,
+ expires_at: typing.Optional[dt.datetime] = OMIT,
+ started_at: typing.Optional[dt.datetime] = OMIT,
+ dnd: typing.Optional[bool] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[UserActivity]:
+ """
+ Paint a badge (and optional glow) on a user's seat for work happening
+ outside Roam — a phone call, a browser meeting, a CRM session. Pass
+ `dnd: true` to also put their assigned office in Do Not Disturb.
+
+ The integration owns the lifecycle: `set` when the session starts,
+ `clear` when it ends. Re-posting the same `externalId` is the heartbeat
+ for long-running sessions — it refreshes `expiresAt` and, unless you
+ send `startedAt`, keeps the original start time. Roam stamps expiry
+ itself (default 10 minutes, maximum 60) so a dropped "ended" webhook
+ cannot leave a permanent glow.
+
+ `externalId` is unique per (integration, user). Two apps can hold
+ activities on the same person at once; you can only update or clear
+ your own rows.
+
+ See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
+ TTL, stacking, and where the indicator appears on the map.
+
+ Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ ASCII email (same convention as `group.create` members). Third-party
+ systems that only have an email do not need a UUID lookup first.
+
+ **Access:** Organization and Personal. Organization tokens may target
+ any user in the workspace. Personal tokens (OAuth or PAT) may target
+ only the token owner.
+
+ **Required scope:** `user:write.activity`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user. Does not require
+ `user:read.email` — email is an identifier, not a
+ disclosure.
+
+ external_id : str
+ Caller-chosen session id, unique per integration and user.
+ Re-using it upserts the existing row (heartbeat). At most
+ 128 Unicode code points.
+
+ display : UserActivityDisplay
+
+ ttl_seconds : typing.Optional[int]
+ Seconds from now until expiry. Mutually exclusive with
+ `expiresAt`. Values above 3600 are **clamped** to 60
+ minutes, not rejected. Default when both are omitted: 600
+ (10 minutes).
+
+ expires_at : typing.Optional[dt.datetime]
+ Absolute expiry (RFC3339, must be in the future). Mutually
+ exclusive with `ttlSeconds`. Instants more than 60 minutes
+ ahead are clamped to that maximum.
+
+ started_at : typing.Optional[dt.datetime]
+ Optional session start (RFC3339). Omit on heartbeats to
+ preserve the original. A future value is clamped to the
+ server's now (clock skew; also so one integration cannot
+ pin the newest-first projection slot).
+
+ dnd : typing.Optional[bool]
+ If true, this activity contributes Do Not Disturb on the
+ user's **own assigned office** until it is cleared or
+ expires. Defaults to false — a badge does not lock an
+ office unless you opt in. Stacks with Zoom/Meet auto-DND
+ and other integrations' DND-flagged rows.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[UserActivity]
+ Activity saved. Body is the live item (same shape `.list` returns
+ per entry), including the server-stamped `startedAt` / `expiresAt`.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "user.activity.set",
+ method="POST",
+ json={
+ "userId": user_id,
+ "externalId": external_id,
+ "display": convert_and_respect_annotation_metadata(
+ object_=display, annotation=UserActivityDisplay, direction="write"
+ ),
+ "ttlSeconds": ttl_seconds,
+ "expiresAt": expires_at,
+ "startedAt": started_at,
+ "dnd": dnd,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ UserActivity,
+ parse_obj_as(
+ type_=UserActivity, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def user_activity_clear(
+ self, *, user_id: str, external_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[None]:
+ """
+ End an activity previously created with [`user.activity.set`](https://developer.ro.am/docs/api/user-activity-set).
+ The row is keyed by this integration plus `userId` and `externalId` —
+ you cannot clear another app's activity.
+
+ Clearing a missing, already-cleared, or already-expired `externalId`
+ still returns **204**. Integrations retry "session ended" webhooks, and
+ the row may have expired in the meantime.
+
+ See [External activity](https://developer.ro.am/docs/guides/user-activity) for TTL, DND
+ stacking, and what happens on the map when the last activity clears.
+
+ **Access:** Organization and Personal. Organization tokens may target
+ any user in the workspace. Personal tokens (OAuth or PAT) may target
+ only the token owner.
+
+ **Required scope:** `user:write.activity`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user.
+
+ external_id : str
+ The `externalId` previously passed to `user.activity.set`.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[None]
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "user.activity.clear",
+ method="POST",
+ json={
+ "userId": user_id,
+ "externalId": external_id,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ return HttpResponse(response=_response, data=None)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def user_activity_list(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[UserActivityListResponse]:
+ """
+ Return every **currently live** external activity for a user — every
+ integration's rows, not only yours. Expired rows are omitted even
+ before the server reaper runs. Not paginated; ordered newest
+ `startedAt` first.
+
+ The map may show fewer entries than this list (the client projection
+ keeps the top three, always including at least one DND-flagged row).
+ `.list` is the source of truth for what is still live.
+
+ See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
+ TTL, and where indicators appear.
+
+ **Access:** Organization and Personal. Organization tokens may list
+ any user in the workspace. Personal tokens (OAuth or PAT) may list
+ only the token owner.
+
+ **Required scope:** `user:read.activity`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal tokens
+ may only pass their own user.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[UserActivityListResponse]
+ Live activities for the user. `activities` is an empty array when none are set.
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "user.activity.list",
+ method="GET",
+ params={
+ "userId": user_id,
+ },
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ UserActivityListResponse,
+ parse_obj_as(
+ type_=UserActivityListResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def messageevent_export(
+ self, *, date: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[str]:
+ """
+ Obtain a daily message event export containing DMs and group
+ chats within your account.
+
+ For customers with archival enabled (please reach out to a Roam
+ ArchiTech to get this process started), at the end of every day,
+ we export all message events for a particular day as a JSON Lines file.
+ This file contains all messages sent:
+ - by a Roam user who is a member of your organization
+ - into a chat containing (at the time of export) at least one Roam user who is a member of your organization
+ - by a bot integration that is part of your organization
+
+ This file also contains message edit and deletion events that meet the above criteria.
+ We specifically exclude waves, room invitations, and other non-message content
+ (that may appear as chats within the Roam application) from the export.
+
+ **Access:** Organization only.
+
+ **Required scope:** `admin:compliance:read`
+
+ ### Message Event Structure
+
+ Each line within the file is a JSON object containing the following fields:
+ - eventType: a string that is one of “sent”, “edited”, or “deleted”
+ - chatId: a UUIDv4 identifier for a particular chat. All messages within the same chat shared the same chatId.
+ - 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.
+ - timestamp: the Unix epoch timestamp when the message was originally sent in numerical format.
+ - messageId: an internal UUIDv4 identifier as a string
+ - sender: a “Participant” object that identifiers the message sender
+ - contentType: a string that is one of the contentTypes associated with the “MessageContent” object
+ - content: a “MessageContent” object that contains the message’s content
+
+ ### Participant
+
+ A Participant is a JSON object that contains three common fields: “participantType”, “id”, and “displayName”
+ - participantType: one of “email”, “bot”, or “occupant”
+ - id: a UUID identifier for the participant
+ - displayName: the name associated with the account or an empty string if not provided
+
+ Depending on the participant type, the object also contains additional fields:
+
+ Email Participant (a human user with a Roam user account)
+ - email: the email of the participant
+
+ Bot Participant (an automated user maintained by the Roam team or created via the Roam API)
+ - roamId: the roam ID associated with the integration
+ - integrationId: a unique integration ID name provided by the bot creator
+ - botCode: a unique identifier
+
+ ### Message Content
+
+ A “MessageContent” object is a JSON object that contains the field “contentType” and,
+ depending on the content type, contains additional fields:
+
+ *Text Content* (contentType = “text”)
+ - text: the text in plaintext
+ - markdownText: the text in Markdown format
+ - attachments: A list of attachment objects
+
+ *Emoji Content* (contentType = “emoji”)
+ - text: text representation of the emoji
+ - colons: emoji in :emoji: format
+ - fileUrl: an optional field containing the URL to a custom emoji image
+
+ *Item Content* (contentType = “item”)
+ - itemUrl: the URL where the file can be downloaded from
+ - itemType: the type of item (e.g. "photo", "pdf", "blob", "video", "audio", etc.)
+
+ *Text Snippet Content* (contentType = "textSnippet")
+ - text: the content of the snippet
+ - language: the language of the snippet
+
+ *Members Changed Content* (contentType = “membersChanged”)
+ - added: a list of Participant objects corresponding to all participants added in this event
+ - removed: a list of Participant objects corresponding to all participants removed in this event
+
+ Parameters
+ ----------
+ date : str
+ The UTC date to fetch the export for in YYYY-MM-DD format.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[str]
+ Export file returned successfully
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ "messageevent.export",
+ method="POST",
+ json={
+ "date": date,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ str,
+ parse_obj_as(
+ type_=str, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+
+class AsyncRawUsersClient:
+ def __init__(self, *, client_wrapper: AsyncClientWrapper):
+ self._client_wrapper = client_wrapper
+
+ async def user_status_set(
+ self,
+ *,
+ user_id: str,
+ will_return: UserStatusSetRequestWillReturn,
+ status: typing.Optional[str] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
- ) -> HttpResponse[UserActivity]:
+ ) -> AsyncHttpResponse[UserStatusSetResponse]:
"""
- Paint a badge (and optional glow) on a user's seat for work happening
- outside Roam — a phone call, a browser meeting, a CRM session. Pass
- `dnd: true` to also put their assigned office in Do Not Disturb.
+ Record an absence on a workspace member — the same Will Return Today /
+ Out of Roam field the desktop client writes, already readable via
+ [`user.info?expand=status`](https://developer.ro.am/docs/api/user-info) and
+ [`user.status.update`](https://developer.ro.am/docs/webhooks/user-status-update).
- The integration owns the lifecycle: `set` when the session starts,
- `clear` when it ends. Re-posting the same `externalId` is the heartbeat
- for long-running sessions — it refreshes `expiresAt` and, unless you
- send `startedAt`, keeps the original start time. Roam stamps expiry
- itself (default 10 minutes, maximum 60) so a dropped "ended" webhook
- cannot leave a permanent glow.
+ This is **not** [external activity](https://developer.ro.am/docs/guides/user-activity). Use
+ `user.status.set` for HR absences (sick leave, vacation, parental leave,
+ public holidays). Use `user.activity.set` for a short-lived on-map glow
+ / emoji (phone call, browser meeting).
+
+ `willReturn` is last-writer-wins with the desktop client. Setting it
+ does **not** check the user out, does **not** enable Do Not Disturb, and
+ does **not** accept a `status` enum (`checkedIn` / `checkedOut` stay
+ read-only).
+
+ `outOfRoam` defaults to `true` (persistent Out of Roam, up to 2 years).
+ Pass `outOfRoam: false` for same-day Will Return Today (`returnTime`
+ must be less than 10 hours from now).
+
+ Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ ASCII email (same convention as `group.create` members). Third-party
+ systems that only have an email do not need a UUID lookup first.
+
+ See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for the two
+ modes, persistence across check-in, and an HRIS example.
+
+ **Access:** Organization and Personal. Organization tokens may target
+ any active member in the workspace. Personal tokens (OAuth or PAT) may
+ target only the token owner.
+
+ **Required scope:** `user:write.status`. Personal Access Tokens skip
+ this check; personal-mode OAuth installs must still request the scope.
+ Reading the field back via `user.info` still needs `user:read.status`.
+
+ Parameters
+ ----------
+ user_id : str
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user. Does not require
+ `user:read.email` — email is an identifier, not a
+ disclosure.
+
+ will_return : UserStatusSetRequestWillReturn
+ Absence to write. Required. Replaces any existing Will
+ Return / Out of Roam on this user.
+
+ status : typing.Optional[str]
+ Rejected. Check-in status is read-only; absences go in
+ `willReturn`. Sending this field returns `400`
+ `invalid_arguments`.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[UserStatusSetResponse]
+ Absence saved. `willReturn` echoes the written value (including
+ defaulted `outOfRoam`). `status` is the user's current check-in
+ (`checkedIn` / `checkedOut`) and is unchanged by this call. `userId`
+ is the canonical UUID.
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ "user.status.set",
+ method="POST",
+ json={
+ "userId": user_id,
+ "willReturn": convert_and_respect_annotation_metadata(
+ object_=will_return, annotation=UserStatusSetRequestWillReturn, direction="write"
+ ),
+ "status": status,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ UserStatusSetResponse,
+ parse_obj_as(
+ type_=UserStatusSetResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 401:
+ raise UnauthorizedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 405:
+ raise MethodNotAllowedError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 429:
+ raise TooManyRequestsError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 500:
+ raise InternalServerError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
- `externalId` is unique per (integration, user). Two apps can hold
- activities on the same person at once; you can only update or clear
- your own rows.
+ async def user_status_clear(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> AsyncHttpResponse[None]:
+ """
+ Remove the Will Return / Out of Roam previously written with
+ [`user.status.set`](https://developer.ro.am/docs/api/user-status-set) or the desktop client.
+ Clears both same-day Will Return Today and persistent Out of Roam.
- See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
- TTL, stacking, and where the indicator appears on the map.
+ Clearing when nothing is set still returns **204**. This call does
+ **not** change check-in status.
+
+ See [Will Return / Out of Roam](https://developer.ro.am/docs/guides/user-status) for
+ persistence, check-in interaction, and the HRIS lifecycle.
**Access:** Organization and Personal. Organization tokens may target
- any user in the workspace. Personal tokens (OAuth or PAT) may target
- only the token owner.
+ any active member in the workspace. Personal tokens (OAuth or PAT) may
+ target only the token owner.
- **Required scope:** `user:write.activity`. Personal Access Tokens skip
+ **Required scope:** `user:write.status`. Personal Access Tokens skip
this check; personal-mode OAuth installs must still request the scope.
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only
- pass their own user.
-
- external_id : str
- Caller-chosen session id, unique per integration and user.
- Re-using it upserts the existing row (heartbeat). At most
- 128 Unicode code points.
-
- display : UserActivityDisplay
-
- ttl_seconds : typing.Optional[int]
- Seconds from now until expiry. Mutually exclusive with
- `expiresAt`. Values above 3600 are **clamped** to 60
- minutes, not rejected. Default when both are omitted: 600
- (10 minutes).
-
- expires_at : typing.Optional[dt.datetime]
- Absolute expiry (RFC3339, must be in the future). Mutually
- exclusive with `ttlSeconds`. Instants more than 60 minutes
- ahead are clamped to that maximum.
-
- started_at : typing.Optional[dt.datetime]
- Optional session start (RFC3339). Omit on heartbeats to
- preserve the original. A future value is clamped to the
- server's now (clock skew; also so one integration cannot
- pin the newest-first projection slot).
-
- dnd : typing.Optional[bool]
- If true, this activity contributes Do Not Disturb on the
- user's **own assigned office** until it is cleared or
- expires. Defaults to false — a badge does not lock an
- office unless you opt in. Stacks with Zoom/Meet auto-DND
- and other integrations' DND-flagged rows.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
Returns
-------
- HttpResponse[UserActivity]
- Activity saved. Body is the live item (same shape `.list` returns
- per entry), including the server-stamped `startedAt` / `expiresAt`.
+ AsyncHttpResponse[None]
"""
- _response = self._client_wrapper.httpx_client.request(
- "user.activity.set",
+ _response = await self._client_wrapper.httpx_client.request(
+ "user.status.clear",
method="POST",
json={
"userId": user_id,
- "externalId": external_id,
- "display": convert_and_respect_annotation_metadata(
- object_=display, annotation=UserActivityDisplay, direction="write"
- ),
- "ttlSeconds": ttl_seconds,
- "expiresAt": expires_at,
- "startedAt": started_at,
- "dnd": dnd,
},
headers={
"content-type": "application/json",
@@ -140,14 +1663,7 @@ def user_activity_set(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
- UserActivity,
- parse_obj_as(
- type_=UserActivity, # type: ignore
- object_=_response.json(),
- ),
- )
- return HttpResponse(response=_response, data=_data)
+ return AsyncHttpResponse(response=_response, data=None)
if _response.status_code == 400:
raise BadRequestError(
headers=dict(_response.headers),
@@ -234,50 +1750,49 @@ def user_activity_set(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
- def user_activity_clear(
- self, *, user_id: str, external_id: str, request_options: typing.Optional[RequestOptions] = None
- ) -> HttpResponse[None]:
+ async def user_status_bubble_set(
+ self,
+ *,
+ user_id: str,
+ text: str,
+ ttl_seconds: typing.Optional[int] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[UserStatusBubbleResponse]:
"""
- End an activity previously created with [`user.activity.set`](https://developer.ro.am/docs/api/user-activity-set).
- The row is keyed by this integration plus `userId` and `externalId` —
- you cannot clear another app's activity.
-
- Clearing a missing, already-cleared, or already-expired `externalId`
- still returns **204**. Integrations retry "session ended" webhooks, and
- the row may have expired in the meantime.
+ Replaces the shared UI/API thought bubble. Text is trimmed and limited to 1–20 Unicode code points. Omitted or null ttlSeconds defaults to 86400; integers from 300 through 86400 are accepted. Durations below 5 minutes or above 24 hours are rejected. Automatic map removal can take up to about a minute after expiration. Repeating set refreshes expiration. Sending expiresAt is rejected.
- See [External activity](https://developer.ro.am/docs/guides/user-activity) for TTL, DND
- stacking, and what happens on the map when the last activity clears.
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
- **Access:** Organization and Personal. Organization tokens may target
- any user in the workspace. Personal tokens (OAuth or PAT) may target
- only the token owner.
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
- **Required scope:** `user:write.activity`. Personal Access Tokens skip
- this check; personal-mode OAuth installs must still request the scope.
+ **Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only
- pass their own user.
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
- external_id : str
- The `externalId` previously passed to `user.activity.set`.
+ text : str
+ Text to trim and store. Must contain 1–20 Unicode code points after trimming. Blank text is rejected.
+
+ ttl_seconds : typing.Optional[int]
+ Optional duration. Omit or pass null for 24 hours. Durations outside 300–86400 seconds are rejected.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
Returns
-------
- HttpResponse[None]
+ AsyncHttpResponse[UserStatusBubbleResponse]
+ Current bubble and canonical user identifier.
"""
- _response = self._client_wrapper.httpx_client.request(
- "user.activity.clear",
+ _response = await self._client_wrapper.httpx_client.request(
+ "user.statusBubble.set",
method="POST",
json={
"userId": user_id,
- "externalId": external_id,
+ "text": text,
+ "ttlSeconds": ttl_seconds,
},
headers={
"content-type": "application/json",
@@ -287,7 +1802,14 @@ def user_activity_clear(
)
try:
if 200 <= _response.status_code < 300:
- return HttpResponse(response=_response, data=None)
+ _data = typing.cast(
+ UserStatusBubbleResponse,
+ parse_obj_as(
+ type_=UserStatusBubbleResponse, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
if _response.status_code == 400:
raise BadRequestError(
headers=dict(_response.headers),
@@ -374,45 +1896,33 @@ def user_activity_clear(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
- def user_activity_list(
+ async def user_status_bubble_get(
self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
- ) -> HttpResponse[UserActivityListResponse]:
+ ) -> AsyncHttpResponse[UserStatusBubbleResponse]:
"""
- Return every **currently live** external activity for a user — every
- integration's rows, not only yours. Expired rows are omitted even
- before the server reaper runs. Not paginated; ordered newest
- `startedAt` first.
+ Returns the shared UI/API thought bubble, or null when no live bubble exists. Expired bubbles are omitted before storage cleanup runs.
- The map may show fewer entries than this list (the client projection
- keeps the top three, always including at least one DND-flagged row).
- `.list` is the source of truth for what is still live.
-
- See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
- TTL, and where indicators appear.
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
- **Access:** Organization and Personal. Organization tokens may list
- any user in the workspace. Personal tokens (OAuth or PAT) may list
- only the token owner.
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
- **Required scope:** `user:read.activity`. Personal Access Tokens skip
- this check; personal-mode OAuth installs must still request the scope.
+ **Required scope:** `user:read.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only pass
- their own user.
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
Returns
-------
- HttpResponse[UserActivityListResponse]
- Live activities for the user. `activities` is an empty array when none are set.
+ AsyncHttpResponse[UserStatusBubbleResponse]
+ Current bubble and canonical user identifier.
"""
- _response = self._client_wrapper.httpx_client.request(
- "user.activity.list",
+ _response = await self._client_wrapper.httpx_client.request(
+ "user.statusBubble.get",
method="GET",
params={
"userId": user_id,
@@ -422,13 +1932,13 @@ def user_activity_list(
try:
if 200 <= _response.status_code < 300:
_data = typing.cast(
- UserActivityListResponse,
+ UserStatusBubbleResponse,
parse_obj_as(
- type_=UserActivityListResponse, # type: ignore
+ type_=UserStatusBubbleResponse, # type: ignore
object_=_response.json(),
),
)
- return HttpResponse(response=_response, data=_data)
+ return AsyncHttpResponse(response=_response, data=_data)
if _response.status_code == 400:
raise BadRequestError(
headers=dict(_response.headers),
@@ -515,103 +2025,35 @@ def user_activity_list(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
- def messageevent_export(
- self, *, date: str, request_options: typing.Optional[RequestOptions] = None
- ) -> HttpResponse[str]:
+ async def user_status_bubble_clear(
+ self, *, user_id: str, request_options: typing.Optional[RequestOptions] = None
+ ) -> AsyncHttpResponse[None]:
"""
- Obtain a daily message event export containing DMs and group
- chats within your account.
-
- For customers with archival enabled (please reach out to a Roam
- ArchiTech to get this process started), at the end of every day,
- we export all message events for a particular day as a JSON Lines file.
- This file contains all messages sent:
- - by a Roam user who is a member of your organization
- - into a chat containing (at the time of export) at least one Roam user who is a member of your organization
- - by a bot integration that is part of your organization
-
- This file also contains message edit and deletion events that meet the above criteria.
- We specifically exclude waves, room invitations, and other non-message content
- (that may appear as chats within the Roam application) from the export.
-
- **Access:** Organization only.
-
- **Required scope:** `admin:compliance:read`
-
- ### Message Event Structure
-
- Each line within the file is a JSON object containing the following fields:
- - eventType: a string that is one of “sent”, “edited”, or “deleted”
- - chatId: a UUIDv4 identifier for a particular chat. All messages within the same chat shared the same chatId.
- - 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.
- - timestamp: the Unix epoch timestamp when the message was originally sent in numerical format.
- - messageId: an internal UUIDv4 identifier as a string
- - sender: a “Participant” object that identifiers the message sender
- - contentType: a string that is one of the contentTypes associated with the “MessageContent” object
- - content: a “MessageContent” object that contains the message’s content
-
- ### Participant
-
- A Participant is a JSON object that contains three common fields: “participantType”, “id”, and “displayName”
- - participantType: one of “email”, “bot”, or “occupant”
- - id: a UUID identifier for the participant
- - displayName: the name associated with the account or an empty string if not provided
-
- Depending on the participant type, the object also contains additional fields:
-
- Email Participant (a human user with a Roam user account)
- - email: the email of the participant
-
- Bot Participant (an automated user maintained by the Roam team or created via the Roam API)
- - roamId: the roam ID associated with the integration
- - integrationId: a unique integration ID name provided by the bot creator
- - botCode: a unique identifier
-
- ### Message Content
-
- A “MessageContent” object is a JSON object that contains the field “contentType” and,
- depending on the content type, contains additional fields:
-
- *Text Content* (contentType = “text”)
- - text: the text in plaintext
- - markdownText: the text in Markdown format
- - attachments: A list of attachment objects
-
- *Emoji Content* (contentType = “emoji”)
- - text: text representation of the emoji
- - colons: emoji in :emoji: format
- - fileUrl: an optional field containing the URL to a custom emoji image
+ Clears the shared thought bubble, including a bubble written by the UI or another integration. Repeating clear is a successful no-op. This does not clear Will Return or external activities.
- *Item Content* (contentType = “item”)
- - itemUrl: the URL where the file can be downloaded from
- - itemType: the type of item (e.g. "photo", "pdf", "blob", "video", "audio", etc.)
+ See [Status bubbles](https://developer.ro.am/docs/guides/user-status-bubble).
- *Text Snippet Content* (contentType = "textSnippet")
- - text: the content of the snippet
- - language: the language of the snippet
+ **Access:** Organization and Personal. Organization credentials may target an active user in the workspace. Personal OAuth and PATs may target only their owner. The workspace comes from the token.
- *Members Changed Content* (contentType = “membersChanged”)
- - added: a list of Participant objects corresponding to all participants added in this event
- - removed: a list of Participant objects corresponding to all participants removed in this event
+ **Required scope:** `user:write.statusBubble`. PATs skip the scope check; personal OAuth requires the scope.
Parameters
----------
- date : str
- The UTC date to fetch the export for in YYYY-MM-DD format.
+ user_id : str
+ Bare UUID, tagged U-… ID, or ASCII email of the target user.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
Returns
-------
- HttpResponse[str]
- Export file returned successfully
+ AsyncHttpResponse[None]
"""
- _response = self._client_wrapper.httpx_client.request(
- "messageevent.export",
+ _response = await self._client_wrapper.httpx_client.request(
+ "user.statusBubble.clear",
method="POST",
json={
- "date": date,
+ "userId": user_id,
},
headers={
"content-type": "application/json",
@@ -621,14 +2063,7 @@ def messageevent_export(
)
try:
if 200 <= _response.status_code < 300:
- _data = typing.cast(
- str,
- parse_obj_as(
- type_=str, # type: ignore
- object_=_response.json(),
- ),
- )
- return HttpResponse(response=_response, data=_data)
+ return AsyncHttpResponse(response=_response, data=None)
if _response.status_code == 400:
raise BadRequestError(
headers=dict(_response.headers),
@@ -651,6 +2086,28 @@ def messageevent_export(
),
),
)
+ if _response.status_code == 403:
+ raise ForbiddenError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ Error,
+ parse_obj_as(
+ type_=Error, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
if _response.status_code == 405:
raise MethodNotAllowedError(
headers=dict(_response.headers),
@@ -693,11 +2150,6 @@ def messageevent_export(
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
-
-class AsyncRawUsersClient:
- def __init__(self, *, client_wrapper: AsyncClientWrapper):
- self._client_wrapper = client_wrapper
-
async def user_activity_set(
self,
*,
@@ -729,6 +2181,10 @@ async def user_activity_set(
See [External activity](https://developer.ro.am/docs/guides/user-activity) for display, DND,
TTL, stacking, and where the indicator appears on the map.
+ Identify the user with `userId`: a bare UUID, tagged `U-…` ID, or
+ ASCII email (same convention as `group.create` members). Third-party
+ systems that only have an email do not need a UUID lookup first.
+
**Access:** Organization and Personal. Organization tokens may target
any user in the workspace. Personal tokens (OAuth or PAT) may target
only the token owner.
@@ -739,8 +2195,11 @@ async def user_activity_set(
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only
- pass their own user.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user. Does not require
+ `user:read.email` — email is an identifier, not a
+ disclosure.
external_id : str
Caller-chosen session id, unique per integration and user.
@@ -923,8 +2382,9 @@ async def user_activity_clear(
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only
- pass their own user.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal
+ tokens may only pass their own user.
external_id : str
The `externalId` previously passed to `user.activity.set`.
@@ -1064,8 +2524,9 @@ async def user_activity_list(
Parameters
----------
user_id : str
- Target user. Bare or tagged UUID. Personal tokens may only pass
- their own user.
+ Target user. Bare UUID, tagged `U-…` ID, or ASCII email
+ (same convention as `group.create` members). Personal tokens
+ may only pass their own user.
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
diff --git a/src/roamhq/users/types/__init__.py b/src/roamhq/users/types/__init__.py
index 916017a..a96afcd 100644
--- a/src/roamhq/users/types/__init__.py
+++ b/src/roamhq/users/types/__init__.py
@@ -9,7 +9,15 @@
if typing.TYPE_CHECKING:
from .user_activity_list_response import UserActivityListResponse
-_dynamic_imports: typing.Dict[str, str] = {"UserActivityListResponse": ".user_activity_list_response"}
+ from .user_status_set_request_will_return import UserStatusSetRequestWillReturn
+ from .user_status_set_response import UserStatusSetResponse
+ from .user_status_set_response_status import UserStatusSetResponseStatus
+_dynamic_imports: typing.Dict[str, str] = {
+ "UserActivityListResponse": ".user_activity_list_response",
+ "UserStatusSetRequestWillReturn": ".user_status_set_request_will_return",
+ "UserStatusSetResponse": ".user_status_set_response",
+ "UserStatusSetResponseStatus": ".user_status_set_response_status",
+}
def __getattr__(attr_name: str) -> typing.Any:
@@ -33,4 +41,9 @@ def __dir__():
return sorted(lazy_attrs)
-__all__ = ["UserActivityListResponse"]
+__all__ = [
+ "UserActivityListResponse",
+ "UserStatusSetRequestWillReturn",
+ "UserStatusSetResponse",
+ "UserStatusSetResponseStatus",
+]
diff --git a/src/roamhq/users/types/user_status_set_request_will_return.py b/src/roamhq/users/types/user_status_set_request_will_return.py
new file mode 100644
index 0000000..851370d
--- /dev/null
+++ b/src/roamhq/users/types/user_status_set_request_will_return.py
@@ -0,0 +1,61 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import datetime as dt
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from ...core.serialization import FieldMetadata
+
+
+class UserStatusSetRequestWillReturn(UniversalBaseModel):
+ """
+ Absence to write. Required. Replaces any existing Will
+ Return / Out of Roam on this user.
+ """
+
+ return_time: typing_extensions.Annotated[
+ dt.datetime,
+ FieldMetadata(alias="returnTime"),
+ pydantic.Field(
+ alias="returnTime",
+ description="When the user is expected back (RFC3339). Must be in\nthe future and at most 2 years from now. Will Return\nToday (`outOfRoam: false`) additionally requires\nless than 10 hours from now.",
+ ),
+ ]
+ """
+ When the user is expected back (RFC3339). Must be in
+ the future and at most 2 years from now. Will Return
+ Today (`outOfRoam: false`) additionally requires
+ less than 10 hours from now.
+ """
+
+ reason: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Optional absence message (for example "On vacation").
+ At most 128 Unicode code points.
+ """
+
+ out_of_roam: typing_extensions.Annotated[
+ typing.Optional[bool],
+ FieldMetadata(alias="outOfRoam"),
+ pydantic.Field(
+ alias="outOfRoam",
+ description="`true` (default) is multi-day Out of Roam and persists\nacross check-ins. `false` is same-day Will Return Today.",
+ ),
+ ] = None
+ """
+ `true` (default) is multi-day Out of Roam and persists
+ across check-ins. `false` is same-day Will Return Today.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/roamhq/users/types/user_status_set_response.py b/src/roamhq/users/types/user_status_set_response.py
new file mode 100644
index 0000000..d4d2917
--- /dev/null
+++ b/src/roamhq/users/types/user_status_set_response.py
@@ -0,0 +1,40 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from ...core.serialization import FieldMetadata
+from ...types.will_return import WillReturn
+from .user_status_set_response_status import UserStatusSetResponseStatus
+
+
+class UserStatusSetResponse(UniversalBaseModel):
+ user_id: typing_extensions.Annotated[
+ str, FieldMetadata(alias="userId"), pydantic.Field(alias="userId", description="Bare UUID of the target user.")
+ ]
+ """
+ Bare UUID of the target user.
+ """
+
+ status: typing.Optional[UserStatusSetResponseStatus] = pydantic.Field(default=None)
+ """
+ Current check-in. Unchanged by this call. Omitted if the
+ presence lookup fails.
+ """
+
+ will_return: typing_extensions.Annotated[
+ WillReturn, FieldMetadata(alias="willReturn"), pydantic.Field(alias="willReturn")
+ ]
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/roamhq/users/types/user_status_set_response_status.py b/src/roamhq/users/types/user_status_set_response_status.py
new file mode 100644
index 0000000..e44ace8
--- /dev/null
+++ b/src/roamhq/users/types/user_status_set_response_status.py
@@ -0,0 +1,7 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+UserStatusSetResponseStatus = typing.Union[typing.Literal["checkedIn", "checkedOut"], typing.Any]
diff --git a/src/roamhq/webhook/__init__.py b/src/roamhq/webhook/__init__.py
index 99f3915..77d28ef 100644
--- a/src/roamhq/webhook/__init__.py
+++ b/src/roamhq/webhook/__init__.py
@@ -13,6 +13,8 @@
DeliveriesWebhookResponseDeliveriesItem,
ListWebhookResponse,
ListWebhookResponseWebhooksItem,
+ WebhookSubscriptionRequestDestination,
+ WebhookSubscriptionRequestDestinationType,
WebhookSubscriptionRequestEvent,
)
_dynamic_imports: typing.Dict[str, str] = {
@@ -20,6 +22,8 @@
"DeliveriesWebhookResponseDeliveriesItem": ".types",
"ListWebhookResponse": ".types",
"ListWebhookResponseWebhooksItem": ".types",
+ "WebhookSubscriptionRequestDestination": ".types",
+ "WebhookSubscriptionRequestDestinationType": ".types",
"WebhookSubscriptionRequestEvent": ".types",
}
@@ -50,5 +54,7 @@ def __dir__():
"DeliveriesWebhookResponseDeliveriesItem",
"ListWebhookResponse",
"ListWebhookResponseWebhooksItem",
+ "WebhookSubscriptionRequestDestination",
+ "WebhookSubscriptionRequestDestinationType",
"WebhookSubscriptionRequestEvent",
]
diff --git a/src/roamhq/webhook/client.py b/src/roamhq/webhook/client.py
index 6271995..9977e03 100644
--- a/src/roamhq/webhook/client.py
+++ b/src/roamhq/webhook/client.py
@@ -11,6 +11,7 @@
from .raw_client import AsyncRawWebhookClient, RawWebhookClient
from .types.deliveries_webhook_response import DeliveriesWebhookResponse
from .types.list_webhook_response import ListWebhookResponse
+from .types.webhook_subscription_request_destination import WebhookSubscriptionRequestDestination
from .types.webhook_subscription_request_event import WebhookSubscriptionRequestEvent
# this is used as the default value for optional parameters
@@ -76,6 +77,7 @@ def subscribe(
event: WebhookSubscriptionRequestEvent,
filter: typing.Optional[WebhookSubscriptionFilter] = OMIT,
api_version: typing.Optional[str] = OMIT,
+ destination: typing.Optional[WebhookSubscriptionRequestDestination] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> Webhook:
"""
@@ -92,7 +94,37 @@ def subscribe(
Roam does not probe the destination URL when you subscribe — the
subscription is created immediately and the first delivery is a real event.
- See the [Webhooks overview](https://developer.ro.am/docs/webhooks/webhooks) for the full list of event names and their filters.
+ Optional `filter` limits which occurrences are delivered. Which keys are
+ valid depends on `event` — see that event's page and the
+ [Event Filters](https://developer.ro.am/docs/webhooks/webhooks#event-filters) table. Omit
+ `filter` to receive every occurrence. An empty object (`{}`) is rejected,
+ as is a filter that does not apply to the event.
+
+ **DMs only:**
+
+ ```json
+ {
+ "url": "https://example.com/hooks/messages",
+ "event": "chat.message",
+ "filter": { "chatType": "dm" }
+ }
+ ```
+
+ **Grok Bot routine** (no ngrok). `destination.token` is write-only — list
+ and subscribe responses echo `destination.type` only. See
+ [Grok](https://developer.ro.am/docs/integrations/grok).
+
+ ```json
+ {
+ "url": "https://api2.cursor.sh/automations/webhook/",
+ "event": "chat.message",
+ "filter": { "self": true },
+ "destination": {
+ "type": "grok_bot",
+ "token": ""
+ }
+ }
+ ```
**Required scope:** `webhook:write`
@@ -105,6 +137,10 @@ def subscribe(
Event to subscribe to.
filter : typing.Optional[WebhookSubscriptionFilter]
+ Optional event-specific filter. Which keys are valid depends on `event`
+ (see the schema). Omit to receive every occurrence; `{}` and `null` are
+ rejected rather than treated as "omitted". Example for DMs only:
+ `{"chatType": "dm"}`.
api_version : typing.Optional[str]
Optional [API version](https://developer.ro.am/docs/guides/api-versioning) (`YYYY-MM-DD`) to pin
@@ -112,6 +148,17 @@ def subscribe(
frozen at your integration's default version. Unsupported values return
`400`.
+ destination : typing.Optional[WebhookSubscriptionRequestDestination]
+ Optional delivery authentication. Omit for Standard Webhooks signed with
+ the API client's `whsec_`. Set `type` to `grok_bot` to deliver to a
+ [Grok Bot](https://developer.ro.am/docs/integrations/grok) webhook-routine URL: Roam signs with
+ the routine's sender key (`Authorization: Bearer` plus
+ `X-Grok-Signature`) or, if `token` is a `whsec_…` Standard Webhooks
+ secret, uses that secret instead of the API client's. The token is
+ write-only — subscribe and list responses echo `destination.type` only.
+ Re-subscribe without this field leaves existing destination auth
+ unchanged; send `"type": ""` to clear it.
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -122,7 +169,7 @@ def subscribe(
Examples
--------
- from roamhq import RoamClient, WebhookSubscriptionFilter
+ from roamhq import RoamClient
client = RoamClient(
roam_version="YOUR_ROAM_VERSION",
@@ -131,13 +178,15 @@ def subscribe(
client.webhook.subscribe(
url="https://example.com/hooks/messages",
event="chat.message",
- filter=WebhookSubscriptionFilter(
- mention=True,
- ),
)
"""
_response = self._raw_client.subscribe(
- url=url, event=event, filter=filter, api_version=api_version, request_options=request_options
+ url=url,
+ event=event,
+ filter=filter,
+ api_version=api_version,
+ destination=destination,
+ request_options=request_options,
)
return _response.data
@@ -326,6 +375,7 @@ async def subscribe(
event: WebhookSubscriptionRequestEvent,
filter: typing.Optional[WebhookSubscriptionFilter] = OMIT,
api_version: typing.Optional[str] = OMIT,
+ destination: typing.Optional[WebhookSubscriptionRequestDestination] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> Webhook:
"""
@@ -342,7 +392,37 @@ async def subscribe(
Roam does not probe the destination URL when you subscribe — the
subscription is created immediately and the first delivery is a real event.
- See the [Webhooks overview](https://developer.ro.am/docs/webhooks/webhooks) for the full list of event names and their filters.
+ Optional `filter` limits which occurrences are delivered. Which keys are
+ valid depends on `event` — see that event's page and the
+ [Event Filters](https://developer.ro.am/docs/webhooks/webhooks#event-filters) table. Omit
+ `filter` to receive every occurrence. An empty object (`{}`) is rejected,
+ as is a filter that does not apply to the event.
+
+ **DMs only:**
+
+ ```json
+ {
+ "url": "https://example.com/hooks/messages",
+ "event": "chat.message",
+ "filter": { "chatType": "dm" }
+ }
+ ```
+
+ **Grok Bot routine** (no ngrok). `destination.token` is write-only — list
+ and subscribe responses echo `destination.type` only. See
+ [Grok](https://developer.ro.am/docs/integrations/grok).
+
+ ```json
+ {
+ "url": "https://api2.cursor.sh/automations/webhook/",
+ "event": "chat.message",
+ "filter": { "self": true },
+ "destination": {
+ "type": "grok_bot",
+ "token": ""
+ }
+ }
+ ```
**Required scope:** `webhook:write`
@@ -355,6 +435,10 @@ async def subscribe(
Event to subscribe to.
filter : typing.Optional[WebhookSubscriptionFilter]
+ Optional event-specific filter. Which keys are valid depends on `event`
+ (see the schema). Omit to receive every occurrence; `{}` and `null` are
+ rejected rather than treated as "omitted". Example for DMs only:
+ `{"chatType": "dm"}`.
api_version : typing.Optional[str]
Optional [API version](https://developer.ro.am/docs/guides/api-versioning) (`YYYY-MM-DD`) to pin
@@ -362,6 +446,17 @@ async def subscribe(
frozen at your integration's default version. Unsupported values return
`400`.
+ destination : typing.Optional[WebhookSubscriptionRequestDestination]
+ Optional delivery authentication. Omit for Standard Webhooks signed with
+ the API client's `whsec_`. Set `type` to `grok_bot` to deliver to a
+ [Grok Bot](https://developer.ro.am/docs/integrations/grok) webhook-routine URL: Roam signs with
+ the routine's sender key (`Authorization: Bearer` plus
+ `X-Grok-Signature`) or, if `token` is a `whsec_…` Standard Webhooks
+ secret, uses that secret instead of the API client's. The token is
+ write-only — subscribe and list responses echo `destination.type` only.
+ Re-subscribe without this field leaves existing destination auth
+ unchanged; send `"type": ""` to clear it.
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -374,7 +469,7 @@ async def subscribe(
--------
import asyncio
- from roamhq import AsyncRoamClient, WebhookSubscriptionFilter
+ from roamhq import AsyncRoamClient
client = AsyncRoamClient(
roam_version="YOUR_ROAM_VERSION",
@@ -386,16 +481,18 @@ async def main() -> None:
await client.webhook.subscribe(
url="https://example.com/hooks/messages",
event="chat.message",
- filter=WebhookSubscriptionFilter(
- mention=True,
- ),
)
asyncio.run(main())
"""
_response = await self._raw_client.subscribe(
- url=url, event=event, filter=filter, api_version=api_version, request_options=request_options
+ url=url,
+ event=event,
+ filter=filter,
+ api_version=api_version,
+ destination=destination,
+ request_options=request_options,
)
return _response.data
diff --git a/src/roamhq/webhook/raw_client.py b/src/roamhq/webhook/raw_client.py
index 1b4a54a..d39060e 100644
--- a/src/roamhq/webhook/raw_client.py
+++ b/src/roamhq/webhook/raw_client.py
@@ -22,6 +22,7 @@
from ..types.webhook_subscription_filter import WebhookSubscriptionFilter
from .types.deliveries_webhook_response import DeliveriesWebhookResponse
from .types.list_webhook_response import ListWebhookResponse
+from .types.webhook_subscription_request_destination import WebhookSubscriptionRequestDestination
from .types.webhook_subscription_request_event import WebhookSubscriptionRequestEvent
from pydantic import ValidationError
@@ -121,6 +122,7 @@ def subscribe(
event: WebhookSubscriptionRequestEvent,
filter: typing.Optional[WebhookSubscriptionFilter] = OMIT,
api_version: typing.Optional[str] = OMIT,
+ destination: typing.Optional[WebhookSubscriptionRequestDestination] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> HttpResponse[Webhook]:
"""
@@ -137,7 +139,37 @@ def subscribe(
Roam does not probe the destination URL when you subscribe — the
subscription is created immediately and the first delivery is a real event.
- See the [Webhooks overview](https://developer.ro.am/docs/webhooks/webhooks) for the full list of event names and their filters.
+ Optional `filter` limits which occurrences are delivered. Which keys are
+ valid depends on `event` — see that event's page and the
+ [Event Filters](https://developer.ro.am/docs/webhooks/webhooks#event-filters) table. Omit
+ `filter` to receive every occurrence. An empty object (`{}`) is rejected,
+ as is a filter that does not apply to the event.
+
+ **DMs only:**
+
+ ```json
+ {
+ "url": "https://example.com/hooks/messages",
+ "event": "chat.message",
+ "filter": { "chatType": "dm" }
+ }
+ ```
+
+ **Grok Bot routine** (no ngrok). `destination.token` is write-only — list
+ and subscribe responses echo `destination.type` only. See
+ [Grok](https://developer.ro.am/docs/integrations/grok).
+
+ ```json
+ {
+ "url": "https://api2.cursor.sh/automations/webhook/",
+ "event": "chat.message",
+ "filter": { "self": true },
+ "destination": {
+ "type": "grok_bot",
+ "token": ""
+ }
+ }
+ ```
**Required scope:** `webhook:write`
@@ -150,6 +182,10 @@ def subscribe(
Event to subscribe to.
filter : typing.Optional[WebhookSubscriptionFilter]
+ Optional event-specific filter. Which keys are valid depends on `event`
+ (see the schema). Omit to receive every occurrence; `{}` and `null` are
+ rejected rather than treated as "omitted". Example for DMs only:
+ `{"chatType": "dm"}`.
api_version : typing.Optional[str]
Optional [API version](https://developer.ro.am/docs/guides/api-versioning) (`YYYY-MM-DD`) to pin
@@ -157,6 +193,17 @@ def subscribe(
frozen at your integration's default version. Unsupported values return
`400`.
+ destination : typing.Optional[WebhookSubscriptionRequestDestination]
+ Optional delivery authentication. Omit for Standard Webhooks signed with
+ the API client's `whsec_`. Set `type` to `grok_bot` to deliver to a
+ [Grok Bot](https://developer.ro.am/docs/integrations/grok) webhook-routine URL: Roam signs with
+ the routine's sender key (`Authorization: Bearer` plus
+ `X-Grok-Signature`) or, if `token` is a `whsec_…` Standard Webhooks
+ secret, uses that secret instead of the API client's. The token is
+ write-only — subscribe and list responses echo `destination.type` only.
+ Re-subscribe without this field leaves existing destination auth
+ unchanged; send `"type": ""` to clear it.
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -172,9 +219,12 @@ def subscribe(
"url": url,
"event": event,
"filter": convert_and_respect_annotation_metadata(
- object_=filter, annotation=typing.Optional[WebhookSubscriptionFilter], direction="write"
+ object_=filter, annotation=WebhookSubscriptionFilter, direction="write"
),
"apiVersion": api_version,
+ "destination": convert_and_respect_annotation_metadata(
+ object_=destination, annotation=WebhookSubscriptionRequestDestination, direction="write"
+ ),
},
headers={
"content-type": "application/json",
@@ -576,6 +626,7 @@ async def subscribe(
event: WebhookSubscriptionRequestEvent,
filter: typing.Optional[WebhookSubscriptionFilter] = OMIT,
api_version: typing.Optional[str] = OMIT,
+ destination: typing.Optional[WebhookSubscriptionRequestDestination] = OMIT,
request_options: typing.Optional[RequestOptions] = None,
) -> AsyncHttpResponse[Webhook]:
"""
@@ -592,7 +643,37 @@ async def subscribe(
Roam does not probe the destination URL when you subscribe — the
subscription is created immediately and the first delivery is a real event.
- See the [Webhooks overview](https://developer.ro.am/docs/webhooks/webhooks) for the full list of event names and their filters.
+ Optional `filter` limits which occurrences are delivered. Which keys are
+ valid depends on `event` — see that event's page and the
+ [Event Filters](https://developer.ro.am/docs/webhooks/webhooks#event-filters) table. Omit
+ `filter` to receive every occurrence. An empty object (`{}`) is rejected,
+ as is a filter that does not apply to the event.
+
+ **DMs only:**
+
+ ```json
+ {
+ "url": "https://example.com/hooks/messages",
+ "event": "chat.message",
+ "filter": { "chatType": "dm" }
+ }
+ ```
+
+ **Grok Bot routine** (no ngrok). `destination.token` is write-only — list
+ and subscribe responses echo `destination.type` only. See
+ [Grok](https://developer.ro.am/docs/integrations/grok).
+
+ ```json
+ {
+ "url": "https://api2.cursor.sh/automations/webhook/",
+ "event": "chat.message",
+ "filter": { "self": true },
+ "destination": {
+ "type": "grok_bot",
+ "token": ""
+ }
+ }
+ ```
**Required scope:** `webhook:write`
@@ -605,6 +686,10 @@ async def subscribe(
Event to subscribe to.
filter : typing.Optional[WebhookSubscriptionFilter]
+ Optional event-specific filter. Which keys are valid depends on `event`
+ (see the schema). Omit to receive every occurrence; `{}` and `null` are
+ rejected rather than treated as "omitted". Example for DMs only:
+ `{"chatType": "dm"}`.
api_version : typing.Optional[str]
Optional [API version](https://developer.ro.am/docs/guides/api-versioning) (`YYYY-MM-DD`) to pin
@@ -612,6 +697,17 @@ async def subscribe(
frozen at your integration's default version. Unsupported values return
`400`.
+ destination : typing.Optional[WebhookSubscriptionRequestDestination]
+ Optional delivery authentication. Omit for Standard Webhooks signed with
+ the API client's `whsec_`. Set `type` to `grok_bot` to deliver to a
+ [Grok Bot](https://developer.ro.am/docs/integrations/grok) webhook-routine URL: Roam signs with
+ the routine's sender key (`Authorization: Bearer` plus
+ `X-Grok-Signature`) or, if `token` is a `whsec_…` Standard Webhooks
+ secret, uses that secret instead of the API client's. The token is
+ write-only — subscribe and list responses echo `destination.type` only.
+ Re-subscribe without this field leaves existing destination auth
+ unchanged; send `"type": ""` to clear it.
+
request_options : typing.Optional[RequestOptions]
Request-specific configuration.
@@ -627,9 +723,12 @@ async def subscribe(
"url": url,
"event": event,
"filter": convert_and_respect_annotation_metadata(
- object_=filter, annotation=typing.Optional[WebhookSubscriptionFilter], direction="write"
+ object_=filter, annotation=WebhookSubscriptionFilter, direction="write"
),
"apiVersion": api_version,
+ "destination": convert_and_respect_annotation_metadata(
+ object_=destination, annotation=WebhookSubscriptionRequestDestination, direction="write"
+ ),
},
headers={
"content-type": "application/json",
diff --git a/src/roamhq/webhook/types/__init__.py b/src/roamhq/webhook/types/__init__.py
index 4ebd27f..7aab8d3 100644
--- a/src/roamhq/webhook/types/__init__.py
+++ b/src/roamhq/webhook/types/__init__.py
@@ -12,12 +12,16 @@
from .deliveries_webhook_response_deliveries_item import DeliveriesWebhookResponseDeliveriesItem
from .list_webhook_response import ListWebhookResponse
from .list_webhook_response_webhooks_item import ListWebhookResponseWebhooksItem
+ from .webhook_subscription_request_destination import WebhookSubscriptionRequestDestination
+ from .webhook_subscription_request_destination_type import WebhookSubscriptionRequestDestinationType
from .webhook_subscription_request_event import WebhookSubscriptionRequestEvent
_dynamic_imports: typing.Dict[str, str] = {
"DeliveriesWebhookResponse": ".deliveries_webhook_response",
"DeliveriesWebhookResponseDeliveriesItem": ".deliveries_webhook_response_deliveries_item",
"ListWebhookResponse": ".list_webhook_response",
"ListWebhookResponseWebhooksItem": ".list_webhook_response_webhooks_item",
+ "WebhookSubscriptionRequestDestination": ".webhook_subscription_request_destination",
+ "WebhookSubscriptionRequestDestinationType": ".webhook_subscription_request_destination_type",
"WebhookSubscriptionRequestEvent": ".webhook_subscription_request_event",
}
@@ -48,5 +52,7 @@ def __dir__():
"DeliveriesWebhookResponseDeliveriesItem",
"ListWebhookResponse",
"ListWebhookResponseWebhooksItem",
+ "WebhookSubscriptionRequestDestination",
+ "WebhookSubscriptionRequestDestinationType",
"WebhookSubscriptionRequestEvent",
]
diff --git a/src/roamhq/webhook/types/webhook_subscription_request_destination.py b/src/roamhq/webhook/types/webhook_subscription_request_destination.py
new file mode 100644
index 0000000..7092a86
--- /dev/null
+++ b/src/roamhq/webhook/types/webhook_subscription_request_destination.py
@@ -0,0 +1,42 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from .webhook_subscription_request_destination_type import WebhookSubscriptionRequestDestinationType
+
+
+class WebhookSubscriptionRequestDestination(UniversalBaseModel):
+ """
+ Optional delivery authentication. Omit for Standard Webhooks signed with
+ the API client's `whsec_`. Set `type` to `grok_bot` to deliver to a
+ [Grok Bot](https://developer.ro.am/docs/integrations/grok) webhook-routine URL: Roam signs with
+ the routine's sender key (`Authorization: Bearer` plus
+ `X-Grok-Signature`) or, if `token` is a `whsec_…` Standard Webhooks
+ secret, uses that secret instead of the API client's. The token is
+ write-only — subscribe and list responses echo `destination.type` only.
+ Re-subscribe without this field leaves existing destination auth
+ unchanged; send `"type": ""` to clear it.
+ """
+
+ type: WebhookSubscriptionRequestDestinationType = pydantic.Field()
+ """
+ Delivery auth scheme.
+ """
+
+ token: str = pydantic.Field()
+ """
+ Grok Bot sender key, or a `whsec_…` Standard Webhooks secret.
+ """
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/roamhq/webhook/types/webhook_subscription_request_destination_type.py b/src/roamhq/webhook/types/webhook_subscription_request_destination_type.py
new file mode 100644
index 0000000..6e4fb44
--- /dev/null
+++ b/src/roamhq/webhook/types/webhook_subscription_request_destination_type.py
@@ -0,0 +1,7 @@
+# This file was auto-generated by Fern from our API Definition.
+
+from __future__ import annotations
+
+import typing
+
+WebhookSubscriptionRequestDestinationType = typing.Union[typing.Literal["grok_bot"], typing.Any]