From 0d5e458080d97ee7cfa90b31dc9629917161ec90 Mon Sep 17 00:00:00 2001 From: roam-sdk-bot Date: Tue, 29 Sep 2026 20:50:10 +0000 Subject: [PATCH] Regenerate SDK from OpenAPI spec Source: WonderInventions/developer-ro-am@3af58388f85aa0afafd3cdfef05c3d3fd959566a Branch: master --- src/roamhq/.fern/metadata.json | 2 +- src/roamhq/__init__.py | 48 +- src/roamhq/client.py | 19 + src/roamhq/core/client_wrapper.py | 1 + src/roamhq/group/client.py | 10 + src/roamhq/group/raw_client.py | 10 + src/roamhq/guest_badges/__init__.py | 36 + src/roamhq/guest_badges/client.py | 651 ++++ src/roamhq/guest_badges/raw_client.py | 1344 ++++++++ src/roamhq/guest_badges/types/__init__.py | 40 + .../types/guest_badge_list_response.py | 36 + .../types/guest_badge_revoke_response.py | 24 + src/roamhq/reference.md | 2971 +++++++++++------ src/roamhq/types/__init__.py | 21 +- src/roamhq/types/guest_badge.py | 68 + src/roamhq/types/user.py | 8 +- src/roamhq/types/user_activity_display.py | 6 +- .../types/user_status_bubble_response.py | 42 + ...er_status_bubble_response_status_bubble.py | 42 + src/roamhq/types/webhook.py | 8 + src/roamhq/types/webhook_destination.py | 28 + src/roamhq/types/webhook_destination_type.py | 7 + .../types/webhook_subscription_filter.py | 26 +- .../{user_will_return.py => will_return.py} | 22 +- src/roamhq/user/client.py | 8 +- src/roamhq/user/raw_client.py | 8 +- src/roamhq/users/__init__.py | 21 +- src/roamhq/users/client.py | 654 +++- src/roamhq/users/raw_client.py | 1921 +++++++++-- src/roamhq/users/types/__init__.py | 17 +- .../user_status_set_request_will_return.py | 61 + .../users/types/user_status_set_response.py | 40 + .../types/user_status_set_response_status.py | 7 + src/roamhq/webhook/__init__.py | 6 + src/roamhq/webhook/client.py | 121 +- src/roamhq/webhook/raw_client.py | 107 +- src/roamhq/webhook/types/__init__.py | 6 + ...ebhook_subscription_request_destination.py | 42 + ...k_subscription_request_destination_type.py | 7 + 39 files changed, 7222 insertions(+), 1274 deletions(-) create mode 100644 src/roamhq/guest_badges/__init__.py create mode 100644 src/roamhq/guest_badges/client.py create mode 100644 src/roamhq/guest_badges/raw_client.py create mode 100644 src/roamhq/guest_badges/types/__init__.py create mode 100644 src/roamhq/guest_badges/types/guest_badge_list_response.py create mode 100644 src/roamhq/guest_badges/types/guest_badge_revoke_response.py create mode 100644 src/roamhq/types/guest_badge.py create mode 100644 src/roamhq/types/user_status_bubble_response.py create mode 100644 src/roamhq/types/user_status_bubble_response_status_bubble.py create mode 100644 src/roamhq/types/webhook_destination.py create mode 100644 src/roamhq/types/webhook_destination_type.py rename src/roamhq/types/{user_will_return.py => will_return.py} (55%) create mode 100644 src/roamhq/users/types/user_status_set_request_will_return.py create mode 100644 src/roamhq/users/types/user_status_set_response.py create mode 100644 src/roamhq/users/types/user_status_set_response_status.py create mode 100644 src/roamhq/webhook/types/webhook_subscription_request_destination.py create mode 100644 src/roamhq/webhook/types/webhook_subscription_request_destination_type.py 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]