From bb8074b817bc550cba91e705ebbd710dee71caba Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 22 May 2026 23:20:25 +0000 Subject: [PATCH 01/26] feat(api): api update --- .stats.yml | 4 +- src/blooio/resources/chats/messages.py | 46 +++++++++++++++++++ .../types/chats/message_list_response.py | 33 ++++++++++++- .../types/chats/message_retrieve_response.py | 33 ++++++++++++- src/blooio/types/chats/message_send_params.py | 41 ++++++++++++++++- .../types/chats/message_send_response.py | 8 ++++ tests/api_resources/chats/test_messages.py | 10 ++++ 7 files changed, 170 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 000c763..8729928 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-bce90b321b6782841d7834e0e0689b1812ef1f10f8f2823cd9908185d9f306e6.yml -openapi_spec_hash: a90c7586ed5e83a938d72a286efa9f8e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-23ac4ce198a5e8bffa1e5b9454a35bd0ca35a31d844f681d60e67cf4ff542411.yml +openapi_spec_hash: e35cf6f966a4c18c696d1c453e40a809 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index 8f844d3..2770375 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -278,6 +278,7 @@ def send( from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, + reply_to: Optional[message_send_params.ReplyTo] | Omit = omit, share_contact: bool | Omit = omit, text: Union[str, SequenceNotStr[str]] | Omit = omit, use_typing_indicator: bool | Omit = omit, @@ -304,6 +305,20 @@ def send( message is delivered without the animation. Effects are not supported in multipart (`parts`) mode. + **Threaded replies (iMessage inline reply):** set the optional `reply_to` field + to send the outgoing message as a reply to a specific earlier message. Two + shapes are accepted: `{ "message_id": "msg_…" }` references a Blooio-minted + message in the same chat (most common — the message*id returned by an earlier + send or surfaced on a `message.received` webhook), or + `{ "guid": "…", "part_index": 0 }` references the raw iMessage GUID for the rare + case where the parent wasn't recorded by Blooio. The reply must target the same + chat and the same from-number as the new send, and the parent must be no older + than 30 days (the iMessage on-device retention horizon). Reply support is + iMessage-only and is rejected on Twilio, dashboard-Twilio, and hybrid send + paths; it's also rejected on multi-message fan-outs (`text` array or per-part + URL-balloon batch). See the `400` responses for the full set of + `reply_target*\\**` error codes. + Args: attachments: Array of attachment URLs or objects with url/name @@ -363,6 +378,13 @@ def send( `text` being a single http(s) URL. Response contains `message_ids[]` + `count` instead of `message_id`. + reply_to: Inline-reply target on `POST /chats/{chatId}/messages`. Pass either `message_id` + (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, + useful for replying to messages received before the row was minted in Blooio). + The new send is dispatched to Lava with the resolved `selectedMessageGuid` + + `partIndex`, which iMessage renders as an inline reply on the recipient's + device. + share_contact: If true, the contact card (Name & Photo) will be shared with this message. The contact card is piggybacked onto the outgoing message. Defaults to false. @@ -391,6 +413,7 @@ def send( "from_number": from_number, "link_preview": link_preview, "parts": parts, + "reply_to": reply_to, "share_contact": share_contact, "text": text, "use_typing_indicator": use_typing_indicator, @@ -653,6 +676,7 @@ async def send( from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, + reply_to: Optional[message_send_params.ReplyTo] | Omit = omit, share_contact: bool | Omit = omit, text: Union[str, SequenceNotStr[str]] | Omit = omit, use_typing_indicator: bool | Omit = omit, @@ -679,6 +703,20 @@ async def send( message is delivered without the animation. Effects are not supported in multipart (`parts`) mode. + **Threaded replies (iMessage inline reply):** set the optional `reply_to` field + to send the outgoing message as a reply to a specific earlier message. Two + shapes are accepted: `{ "message_id": "msg_…" }` references a Blooio-minted + message in the same chat (most common — the message*id returned by an earlier + send or surfaced on a `message.received` webhook), or + `{ "guid": "…", "part_index": 0 }` references the raw iMessage GUID for the rare + case where the parent wasn't recorded by Blooio. The reply must target the same + chat and the same from-number as the new send, and the parent must be no older + than 30 days (the iMessage on-device retention horizon). Reply support is + iMessage-only and is rejected on Twilio, dashboard-Twilio, and hybrid send + paths; it's also rejected on multi-message fan-outs (`text` array or per-part + URL-balloon batch). See the `400` responses for the full set of + `reply_target*\\**` error codes. + Args: attachments: Array of attachment URLs or objects with url/name @@ -738,6 +776,13 @@ async def send( `text` being a single http(s) URL. Response contains `message_ids[]` + `count` instead of `message_id`. + reply_to: Inline-reply target on `POST /chats/{chatId}/messages`. Pass either `message_id` + (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, + useful for replying to messages received before the row was minted in Blooio). + The new send is dispatched to Lava with the resolved `selectedMessageGuid` + + `partIndex`, which iMessage renders as an inline reply on the recipient's + device. + share_contact: If true, the contact card (Name & Photo) will be shared with this message. The contact card is piggybacked onto the outgoing message. Defaults to false. @@ -766,6 +811,7 @@ async def send( "from_number": from_number, "link_preview": link_preview, "parts": parts, + "reply_to": reply_to, "share_contact": share_contact, "text": text, "use_typing_indicator": use_typing_indicator, diff --git a/src/blooio/types/chats/message_list_response.py b/src/blooio/types/chats/message_list_response.py index 88001a7..b31467d 100644 --- a/src/blooio/types/chats/message_list_response.py +++ b/src/blooio/types/chats/message_list_response.py @@ -7,7 +7,31 @@ from ..._models import BaseModel from ..pagination import Pagination -__all__ = ["MessageListResponse", "Message"] +__all__ = ["MessageListResponse", "Message", "MessageReplyTo"] + + +class MessageReplyTo(BaseModel): + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that returns a single message or a list of messages. + """ + + guid: Optional[str] = None + """The raw iMessage GUID of the parent. + + Always populated on real inline replies; the on-device record-of-truth + identifier that survives even when `message_id` cannot be resolved. + """ + + message_id: Optional[str] = None + """The Blooio `message_id` of the parent message. + + NULL when the parent isn't in our `messages` table (e.g., the original was sent + from outside Blooio's pipeline). + """ + + part_index: int + """Which part of the parent was replied to. 0 for the common single-part case.""" class Message(BaseModel): @@ -30,6 +54,13 @@ class Message(BaseModel): reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" + reply_to: Optional[MessageReplyTo] = None + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that + returns a single message or a list of messages. + """ + sender: Optional[str] = None """Sender's phone number or email for inbound group messages. diff --git a/src/blooio/types/chats/message_retrieve_response.py b/src/blooio/types/chats/message_retrieve_response.py index a2c61ad..aee571a 100644 --- a/src/blooio/types/chats/message_retrieve_response.py +++ b/src/blooio/types/chats/message_retrieve_response.py @@ -6,7 +6,7 @@ from .reaction import Reaction from ..._models import BaseModel -__all__ = ["MessageRetrieveResponse", "Contact"] +__all__ = ["MessageRetrieveResponse", "Contact", "ReplyTo"] class Contact(BaseModel): @@ -18,6 +18,30 @@ class Contact(BaseModel): name: Optional[str] = None +class ReplyTo(BaseModel): + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that returns a single message or a list of messages. + """ + + guid: Optional[str] = None + """The raw iMessage GUID of the parent. + + Always populated on real inline replies; the on-device record-of-truth + identifier that survives even when `message_id` cannot be resolved. + """ + + message_id: Optional[str] = None + """The Blooio `message_id` of the parent message. + + NULL when the parent isn't in our `messages` table (e.g., the original was sent + from outside Blooio's pipeline). + """ + + part_index: int + """Which part of the parent was replied to. 0 for the common single-part case.""" + + class MessageRetrieveResponse(BaseModel): attachments: Optional[List[object]] = None @@ -39,6 +63,13 @@ class MessageRetrieveResponse(BaseModel): reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" + reply_to: Optional[ReplyTo] = None + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that + returns a single message or a list of messages. + """ + sender: Optional[str] = None """Sender's phone number or email for inbound group messages. diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py index b2b935b..fd31ffd 100644 --- a/src/blooio/types/chats/message_send_params.py +++ b/src/blooio/types/chats/message_send_params.py @@ -9,7 +9,7 @@ from ..._utils import PropertyInfo from .link_preview_param import LinkPreviewParam -__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionObjectVariant1", "Part"] +__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionObjectVariant1", "Part", "ReplyTo"] class MessageSendParams(TypedDict, total=False): @@ -97,6 +97,16 @@ class MessageSendParams(TypedDict, total=False): `count` instead of `message_id`. """ + reply_to: Optional[ReplyTo] + """Inline-reply target on `POST /chats/{chatId}/messages`. + + Pass either `message_id` (preferred — references a Blooio-minted message) or + `guid` (raw iMessage GUID, useful for replying to messages received before the + row was minted in Blooio). The new send is dispatched to Lava with the resolved + `selectedMessageGuid` + `partIndex`, which iMessage renders as an inline reply + on the recipient's device. + """ + share_contact: bool """If true, the contact card (Name & Photo) will be shared with this message. @@ -149,3 +159,32 @@ class Part(TypedDict, total=False): url: str """URL to an attachment for this part. Mutually exclusive with 'text'.""" + + +class ReplyTo(TypedDict, total=False): + """Inline-reply target on `POST /chats/{chatId}/messages`. + + Pass either `message_id` (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, useful for replying to messages received before the row was minted in Blooio). The new send is dispatched to Lava with the resolved `selectedMessageGuid` + `partIndex`, which iMessage renders as an inline reply on the recipient's device. + """ + + guid: str + """Raw iMessage GUID of the parent. + + When supplied without a `message_id`, Blooio attempts to look up the parent via + `provider_message_guid`; if the parent isn't in our table the send still + proceeds (Lava will thread on the device when possible) and the response carries + `parent_unresolved: true`. + """ + + message_id: str + """Blooio `message_id` of the parent. + + Must belong to the same chat, same from-number, and be no older than 30 days. + Returns 404 `reply_target_not_found` if unknown. + """ + + part_index: int + """Which part of the parent to reply to. + + Defaults to 0 (covers the 99% case of replying to a single-part text message). + """ diff --git a/src/blooio/types/chats/message_send_response.py b/src/blooio/types/chats/message_send_response.py index 385d693..04f8395 100644 --- a/src/blooio/types/chats/message_send_response.py +++ b/src/blooio/types/chats/message_send_response.py @@ -30,6 +30,14 @@ class MessageSendResponse(BaseModel): (URL-balloon batch mode). """ + parent_unresolved: Optional[bool] = None + """ + Present (and `true`) only when `reply_to.guid` was supplied without a + `message_id` and the GUID didn't map to any Blooio-minted row. The send still + proceeds and the device may still thread it; this flag signals that Blooio + couldn't link the new message to a known parent. + """ + participants: Optional[List[str]] = None """List of participants (present for multi-recipient)""" diff --git a/tests/api_resources/chats/test_messages.py b/tests/api_resources/chats/test_messages.py index 1ca5b92..fddd22b 100644 --- a/tests/api_resources/chats/test_messages.py +++ b/tests/api_resources/chats/test_messages.py @@ -283,6 +283,11 @@ def test_method_send_with_all_params(self, client: Blooio) -> None: "url": "url", } ], + reply_to={ + "guid": "guid", + "message_id": "message_id", + "part_index": 0, + }, share_contact=True, text="string", use_typing_indicator=True, @@ -590,6 +595,11 @@ async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> N "url": "url", } ], + reply_to={ + "guid": "guid", + "message_id": "message_id", + "part_index": 0, + }, share_contact=True, text="string", use_typing_indicator=True, From e4708cb01510e75961ac6a058308140cf69424ee Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 10:20:21 +0000 Subject: [PATCH 02/26] feat(api): api update --- .stats.yml | 4 ++-- src/blooio/types/chat_list_response.py | 6 ++++++ src/blooio/types/chat_retrieve_response.py | 6 ++++++ src/blooio/types/chats/chat_background_response.py | 8 ++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8729928..77ef01e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-23ac4ce198a5e8bffa1e5b9454a35bd0ca35a31d844f681d60e67cf4ff542411.yml -openapi_spec_hash: e35cf6f966a4c18c696d1c453e40a809 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-81b1f2ba01495b54ab7b259e5cc92a6d6f114b26b8d41ffc4fee80d9f9b3db20.yml +openapi_spec_hash: 173a216fb869614c676de670dfa814b5 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/types/chat_list_response.py b/src/blooio/types/chat_list_response.py index 6ec4a82..ebc1194 100644 --- a/src/blooio/types/chat_list_response.py +++ b/src/blooio/types/chat_list_response.py @@ -24,6 +24,12 @@ class Chat(BaseModel): id: Optional[str] = None """Chat identifier (phone number, email, or group ID)""" + background_id: Optional[str] = None + """Identifier for the active chat background""" + + background_url: Optional[str] = None + """Public URL of the chat background image (if one has been set via the API)""" + contact: Optional[ChatContact] = None """Contact info (only for non-group chats)""" diff --git a/src/blooio/types/chat_retrieve_response.py b/src/blooio/types/chat_retrieve_response.py index 14f98a5..676d1f1 100644 --- a/src/blooio/types/chat_retrieve_response.py +++ b/src/blooio/types/chat_retrieve_response.py @@ -23,6 +23,12 @@ class ChatRetrieveResponse(BaseModel): id: Optional[str] = None """Chat identifier (phone number, email, or group ID)""" + background_id: Optional[str] = None + """Identifier for the active chat background""" + + background_url: Optional[str] = None + """Public URL of the chat background image (if one has been set via the API)""" + contact: Optional[Contact] = None """Contact info (only for non-group chats)""" diff --git a/src/blooio/types/chats/chat_background_response.py b/src/blooio/types/chats/chat_background_response.py index 48c1348..26ffacd 100644 --- a/src/blooio/types/chats/chat_background_response.py +++ b/src/blooio/types/chats/chat_background_response.py @@ -13,6 +13,14 @@ class ChatBackgroundResponse(BaseModel): background_id: Optional[str] = None """Unique identifier for the current background, or null if none""" + background_url: Optional[str] = None + """Public URL of the persisted background image stored in R2. + + Returned after a successful PUT and on GET when a background has been set + through the API. May be null if persistence failed or the background was set + outside of the API. + """ + background_version: Optional[int] = None """Version number of the background (for cache invalidation)""" From 03aecefb518d40f36c37d71cfa7637cefb0575c4 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 20:20:20 +0000 Subject: [PATCH 03/26] feat(api): api update --- .stats.yml | 4 +- src/blooio/resources/chats/background.py | 50 +++++++++++++++---- src/blooio/resources/chats/chats.py | 12 ++--- src/blooio/resources/chats/messages.py | 46 ----------------- src/blooio/types/chat_list_response.py | 6 --- src/blooio/types/chat_retrieve_response.py | 6 --- .../types/chats/background_set_params.py | 7 ++- .../types/chats/chat_background_response.py | 8 --- .../types/chats/message_list_response.py | 33 +----------- .../types/chats/message_retrieve_response.py | 33 +----------- src/blooio/types/chats/message_send_params.py | 41 +-------------- .../types/chats/message_send_response.py | 8 --- tests/api_resources/chats/test_messages.py | 10 ---- 13 files changed, 57 insertions(+), 207 deletions(-) diff --git a/.stats.yml b/.stats.yml index 77ef01e..36f5ef8 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-81b1f2ba01495b54ab7b259e5cc92a6d6f114b26b8d41ffc4fee80d9f9b3db20.yml -openapi_spec_hash: 173a216fb869614c676de670dfa814b5 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-164b95920b6f8c3bcf47d73fb582375e0e40e72a5d01a19ada82c19431b5853e.yml +openapi_spec_hash: 988aee0a657a2411a646e306ed9e2e22 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/background.py b/src/blooio/resources/chats/background.py index a0c1121..52fa8ba 100644 --- a/src/blooio/resources/chats/background.py +++ b/src/blooio/resources/chats/background.py @@ -25,7 +25,7 @@ class BackgroundResource(SyncAPIResource): - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" @cached_property def with_raw_response(self) -> BackgroundResourceWithRawResponse: @@ -132,12 +132,27 @@ def set( Works for both 1-on-1 and group chats. - The uploaded image is converted into a PosterKit-compatible archive and applied - to the iMessage conversation on the linked device. Supported formats: JPEG, PNG, - GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + The request body must be `multipart/form-data` with a single `background` field + containing the **raw image file bytes** (not a URL or base64 string). Supported + formats: JPEG, PNG, GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + + **Example with curl** — note the `@` prefix that tells curl to read the file + from disk: + + ```bash + curl -X PUT "https://backend.blooio.com/v2/api/chats/%2B15551234567/background" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -F "background=@/path/to/image.jpg;type=image/jpeg" + ``` + + When the chat id is a phone number, percent-encode the leading `+` as `%2B` in + the URL path. Args: - background: The image file to set as the chat background + background: Binary image file upload (JPEG, PNG, GIF, WebP, HEIC/HEIF, max 10 MB). Send as a + file field in `multipart/form-data` — e.g. `-F "background=@/path/to/image.jpg"` + with curl, or a `File`/`Blob` appended to `FormData` in JavaScript. Do NOT send + a URL or base64 string. extra_headers: Send extra headers @@ -167,7 +182,7 @@ def set( class AsyncBackgroundResource(AsyncAPIResource): - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" @cached_property def with_raw_response(self) -> AsyncBackgroundResourceWithRawResponse: @@ -274,12 +289,27 @@ async def set( Works for both 1-on-1 and group chats. - The uploaded image is converted into a PosterKit-compatible archive and applied - to the iMessage conversation on the linked device. Supported formats: JPEG, PNG, - GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + The request body must be `multipart/form-data` with a single `background` field + containing the **raw image file bytes** (not a URL or base64 string). Supported + formats: JPEG, PNG, GIF, WebP, HEIC/HEIF. Maximum file size: 10 MB. + + **Example with curl** — note the `@` prefix that tells curl to read the file + from disk: + + ```bash + curl -X PUT "https://backend.blooio.com/v2/api/chats/%2B15551234567/background" \\ + -H "Authorization: Bearer YOUR_API_KEY" \\ + -F "background=@/path/to/image.jpg;type=image/jpeg" + ``` + + When the chat id is a phone number, percent-encode the leading `+` as `%2B` in + the URL path. Args: - background: The image file to set as the chat background + background: Binary image file upload (JPEG, PNG, GIF, WebP, HEIC/HEIF, max 10 MB). Send as a + file field in `multipart/form-data` — e.g. `-F "background=@/path/to/image.jpg"` + with curl, or a `File`/`Blob` appended to `FormData` in JavaScript. Do NOT send + a URL or base64 string. extra_headers: Send extra headers diff --git a/src/blooio/resources/chats/chats.py b/src/blooio/resources/chats/chats.py index 95f2bce..fb775fb 100644 --- a/src/blooio/resources/chats/chats.py +++ b/src/blooio/resources/chats/chats.py @@ -78,7 +78,7 @@ def typing(self) -> TypingResource: @cached_property def background(self) -> BackgroundResource: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return BackgroundResource(self._client) @cached_property @@ -279,7 +279,7 @@ def typing(self) -> AsyncTypingResource: @cached_property def background(self) -> AsyncBackgroundResource: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return AsyncBackgroundResource(self._client) @cached_property @@ -496,7 +496,7 @@ def typing(self) -> TypingResourceWithRawResponse: @cached_property def background(self) -> BackgroundResourceWithRawResponse: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return BackgroundResourceWithRawResponse(self._chats.background) @@ -536,7 +536,7 @@ def typing(self) -> AsyncTypingResourceWithRawResponse: @cached_property def background(self) -> AsyncBackgroundResourceWithRawResponse: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return AsyncBackgroundResourceWithRawResponse(self._chats.background) @@ -576,7 +576,7 @@ def typing(self) -> TypingResourceWithStreamingResponse: @cached_property def background(self) -> BackgroundResourceWithStreamingResponse: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return BackgroundResourceWithStreamingResponse(self._chats.background) @@ -616,5 +616,5 @@ def typing(self) -> AsyncTypingResourceWithStreamingResponse: @cached_property def background(self) -> AsyncBackgroundResourceWithStreamingResponse: - """Set, get, and remove conversation backgrounds""" + """View conversations and messages""" return AsyncBackgroundResourceWithStreamingResponse(self._chats.background) diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index 2770375..8f844d3 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -278,7 +278,6 @@ def send( from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, - reply_to: Optional[message_send_params.ReplyTo] | Omit = omit, share_contact: bool | Omit = omit, text: Union[str, SequenceNotStr[str]] | Omit = omit, use_typing_indicator: bool | Omit = omit, @@ -305,20 +304,6 @@ def send( message is delivered without the animation. Effects are not supported in multipart (`parts`) mode. - **Threaded replies (iMessage inline reply):** set the optional `reply_to` field - to send the outgoing message as a reply to a specific earlier message. Two - shapes are accepted: `{ "message_id": "msg_…" }` references a Blooio-minted - message in the same chat (most common — the message*id returned by an earlier - send or surfaced on a `message.received` webhook), or - `{ "guid": "…", "part_index": 0 }` references the raw iMessage GUID for the rare - case where the parent wasn't recorded by Blooio. The reply must target the same - chat and the same from-number as the new send, and the parent must be no older - than 30 days (the iMessage on-device retention horizon). Reply support is - iMessage-only and is rejected on Twilio, dashboard-Twilio, and hybrid send - paths; it's also rejected on multi-message fan-outs (`text` array or per-part - URL-balloon batch). See the `400` responses for the full set of - `reply_target*\\**` error codes. - Args: attachments: Array of attachment URLs or objects with url/name @@ -378,13 +363,6 @@ def send( `text` being a single http(s) URL. Response contains `message_ids[]` + `count` instead of `message_id`. - reply_to: Inline-reply target on `POST /chats/{chatId}/messages`. Pass either `message_id` - (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, - useful for replying to messages received before the row was minted in Blooio). - The new send is dispatched to Lava with the resolved `selectedMessageGuid` + - `partIndex`, which iMessage renders as an inline reply on the recipient's - device. - share_contact: If true, the contact card (Name & Photo) will be shared with this message. The contact card is piggybacked onto the outgoing message. Defaults to false. @@ -413,7 +391,6 @@ def send( "from_number": from_number, "link_preview": link_preview, "parts": parts, - "reply_to": reply_to, "share_contact": share_contact, "text": text, "use_typing_indicator": use_typing_indicator, @@ -676,7 +653,6 @@ async def send( from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, - reply_to: Optional[message_send_params.ReplyTo] | Omit = omit, share_contact: bool | Omit = omit, text: Union[str, SequenceNotStr[str]] | Omit = omit, use_typing_indicator: bool | Omit = omit, @@ -703,20 +679,6 @@ async def send( message is delivered without the animation. Effects are not supported in multipart (`parts`) mode. - **Threaded replies (iMessage inline reply):** set the optional `reply_to` field - to send the outgoing message as a reply to a specific earlier message. Two - shapes are accepted: `{ "message_id": "msg_…" }` references a Blooio-minted - message in the same chat (most common — the message*id returned by an earlier - send or surfaced on a `message.received` webhook), or - `{ "guid": "…", "part_index": 0 }` references the raw iMessage GUID for the rare - case where the parent wasn't recorded by Blooio. The reply must target the same - chat and the same from-number as the new send, and the parent must be no older - than 30 days (the iMessage on-device retention horizon). Reply support is - iMessage-only and is rejected on Twilio, dashboard-Twilio, and hybrid send - paths; it's also rejected on multi-message fan-outs (`text` array or per-part - URL-balloon batch). See the `400` responses for the full set of - `reply_target*\\**` error codes. - Args: attachments: Array of attachment URLs or objects with url/name @@ -776,13 +738,6 @@ async def send( `text` being a single http(s) URL. Response contains `message_ids[]` + `count` instead of `message_id`. - reply_to: Inline-reply target on `POST /chats/{chatId}/messages`. Pass either `message_id` - (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, - useful for replying to messages received before the row was minted in Blooio). - The new send is dispatched to Lava with the resolved `selectedMessageGuid` + - `partIndex`, which iMessage renders as an inline reply on the recipient's - device. - share_contact: If true, the contact card (Name & Photo) will be shared with this message. The contact card is piggybacked onto the outgoing message. Defaults to false. @@ -811,7 +766,6 @@ async def send( "from_number": from_number, "link_preview": link_preview, "parts": parts, - "reply_to": reply_to, "share_contact": share_contact, "text": text, "use_typing_indicator": use_typing_indicator, diff --git a/src/blooio/types/chat_list_response.py b/src/blooio/types/chat_list_response.py index ebc1194..6ec4a82 100644 --- a/src/blooio/types/chat_list_response.py +++ b/src/blooio/types/chat_list_response.py @@ -24,12 +24,6 @@ class Chat(BaseModel): id: Optional[str] = None """Chat identifier (phone number, email, or group ID)""" - background_id: Optional[str] = None - """Identifier for the active chat background""" - - background_url: Optional[str] = None - """Public URL of the chat background image (if one has been set via the API)""" - contact: Optional[ChatContact] = None """Contact info (only for non-group chats)""" diff --git a/src/blooio/types/chat_retrieve_response.py b/src/blooio/types/chat_retrieve_response.py index 676d1f1..14f98a5 100644 --- a/src/blooio/types/chat_retrieve_response.py +++ b/src/blooio/types/chat_retrieve_response.py @@ -23,12 +23,6 @@ class ChatRetrieveResponse(BaseModel): id: Optional[str] = None """Chat identifier (phone number, email, or group ID)""" - background_id: Optional[str] = None - """Identifier for the active chat background""" - - background_url: Optional[str] = None - """Public URL of the chat background image (if one has been set via the API)""" - contact: Optional[Contact] = None """Contact info (only for non-group chats)""" diff --git a/src/blooio/types/chats/background_set_params.py b/src/blooio/types/chats/background_set_params.py index 315fc10..5820f62 100644 --- a/src/blooio/types/chats/background_set_params.py +++ b/src/blooio/types/chats/background_set_params.py @@ -11,4 +11,9 @@ class BackgroundSetParams(TypedDict, total=False): background: Required[FileTypes] - """The image file to set as the chat background""" + """Binary image file upload (JPEG, PNG, GIF, WebP, HEIC/HEIF, max 10 MB). + + Send as a file field in `multipart/form-data` — e.g. + `-F "background=@/path/to/image.jpg"` with curl, or a `File`/`Blob` appended to + `FormData` in JavaScript. Do NOT send a URL or base64 string. + """ diff --git a/src/blooio/types/chats/chat_background_response.py b/src/blooio/types/chats/chat_background_response.py index 26ffacd..48c1348 100644 --- a/src/blooio/types/chats/chat_background_response.py +++ b/src/blooio/types/chats/chat_background_response.py @@ -13,14 +13,6 @@ class ChatBackgroundResponse(BaseModel): background_id: Optional[str] = None """Unique identifier for the current background, or null if none""" - background_url: Optional[str] = None - """Public URL of the persisted background image stored in R2. - - Returned after a successful PUT and on GET when a background has been set - through the API. May be null if persistence failed or the background was set - outside of the API. - """ - background_version: Optional[int] = None """Version number of the background (for cache invalidation)""" diff --git a/src/blooio/types/chats/message_list_response.py b/src/blooio/types/chats/message_list_response.py index b31467d..88001a7 100644 --- a/src/blooio/types/chats/message_list_response.py +++ b/src/blooio/types/chats/message_list_response.py @@ -7,31 +7,7 @@ from ..._models import BaseModel from ..pagination import Pagination -__all__ = ["MessageListResponse", "Message", "MessageReplyTo"] - - -class MessageReplyTo(BaseModel): - """Inline-reply parent reference. - - Identical shape on `message.received` webhooks and on every GET endpoint that returns a single message or a list of messages. - """ - - guid: Optional[str] = None - """The raw iMessage GUID of the parent. - - Always populated on real inline replies; the on-device record-of-truth - identifier that survives even when `message_id` cannot be resolved. - """ - - message_id: Optional[str] = None - """The Blooio `message_id` of the parent message. - - NULL when the parent isn't in our `messages` table (e.g., the original was sent - from outside Blooio's pipeline). - """ - - part_index: int - """Which part of the parent was replied to. 0 for the common single-part case.""" +__all__ = ["MessageListResponse", "Message"] class Message(BaseModel): @@ -54,13 +30,6 @@ class Message(BaseModel): reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" - reply_to: Optional[MessageReplyTo] = None - """Inline-reply parent reference. - - Identical shape on `message.received` webhooks and on every GET endpoint that - returns a single message or a list of messages. - """ - sender: Optional[str] = None """Sender's phone number or email for inbound group messages. diff --git a/src/blooio/types/chats/message_retrieve_response.py b/src/blooio/types/chats/message_retrieve_response.py index aee571a..a2c61ad 100644 --- a/src/blooio/types/chats/message_retrieve_response.py +++ b/src/blooio/types/chats/message_retrieve_response.py @@ -6,7 +6,7 @@ from .reaction import Reaction from ..._models import BaseModel -__all__ = ["MessageRetrieveResponse", "Contact", "ReplyTo"] +__all__ = ["MessageRetrieveResponse", "Contact"] class Contact(BaseModel): @@ -18,30 +18,6 @@ class Contact(BaseModel): name: Optional[str] = None -class ReplyTo(BaseModel): - """Inline-reply parent reference. - - Identical shape on `message.received` webhooks and on every GET endpoint that returns a single message or a list of messages. - """ - - guid: Optional[str] = None - """The raw iMessage GUID of the parent. - - Always populated on real inline replies; the on-device record-of-truth - identifier that survives even when `message_id` cannot be resolved. - """ - - message_id: Optional[str] = None - """The Blooio `message_id` of the parent message. - - NULL when the parent isn't in our `messages` table (e.g., the original was sent - from outside Blooio's pipeline). - """ - - part_index: int - """Which part of the parent was replied to. 0 for the common single-part case.""" - - class MessageRetrieveResponse(BaseModel): attachments: Optional[List[object]] = None @@ -63,13 +39,6 @@ class MessageRetrieveResponse(BaseModel): reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" - reply_to: Optional[ReplyTo] = None - """Inline-reply parent reference. - - Identical shape on `message.received` webhooks and on every GET endpoint that - returns a single message or a list of messages. - """ - sender: Optional[str] = None """Sender's phone number or email for inbound group messages. diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py index fd31ffd..b2b935b 100644 --- a/src/blooio/types/chats/message_send_params.py +++ b/src/blooio/types/chats/message_send_params.py @@ -9,7 +9,7 @@ from ..._utils import PropertyInfo from .link_preview_param import LinkPreviewParam -__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionObjectVariant1", "Part", "ReplyTo"] +__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionObjectVariant1", "Part"] class MessageSendParams(TypedDict, total=False): @@ -97,16 +97,6 @@ class MessageSendParams(TypedDict, total=False): `count` instead of `message_id`. """ - reply_to: Optional[ReplyTo] - """Inline-reply target on `POST /chats/{chatId}/messages`. - - Pass either `message_id` (preferred — references a Blooio-minted message) or - `guid` (raw iMessage GUID, useful for replying to messages received before the - row was minted in Blooio). The new send is dispatched to Lava with the resolved - `selectedMessageGuid` + `partIndex`, which iMessage renders as an inline reply - on the recipient's device. - """ - share_contact: bool """If true, the contact card (Name & Photo) will be shared with this message. @@ -159,32 +149,3 @@ class Part(TypedDict, total=False): url: str """URL to an attachment for this part. Mutually exclusive with 'text'.""" - - -class ReplyTo(TypedDict, total=False): - """Inline-reply target on `POST /chats/{chatId}/messages`. - - Pass either `message_id` (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, useful for replying to messages received before the row was minted in Blooio). The new send is dispatched to Lava with the resolved `selectedMessageGuid` + `partIndex`, which iMessage renders as an inline reply on the recipient's device. - """ - - guid: str - """Raw iMessage GUID of the parent. - - When supplied without a `message_id`, Blooio attempts to look up the parent via - `provider_message_guid`; if the parent isn't in our table the send still - proceeds (Lava will thread on the device when possible) and the response carries - `parent_unresolved: true`. - """ - - message_id: str - """Blooio `message_id` of the parent. - - Must belong to the same chat, same from-number, and be no older than 30 days. - Returns 404 `reply_target_not_found` if unknown. - """ - - part_index: int - """Which part of the parent to reply to. - - Defaults to 0 (covers the 99% case of replying to a single-part text message). - """ diff --git a/src/blooio/types/chats/message_send_response.py b/src/blooio/types/chats/message_send_response.py index 04f8395..385d693 100644 --- a/src/blooio/types/chats/message_send_response.py +++ b/src/blooio/types/chats/message_send_response.py @@ -30,14 +30,6 @@ class MessageSendResponse(BaseModel): (URL-balloon batch mode). """ - parent_unresolved: Optional[bool] = None - """ - Present (and `true`) only when `reply_to.guid` was supplied without a - `message_id` and the GUID didn't map to any Blooio-minted row. The send still - proceeds and the device may still thread it; this flag signals that Blooio - couldn't link the new message to a known parent. - """ - participants: Optional[List[str]] = None """List of participants (present for multi-recipient)""" diff --git a/tests/api_resources/chats/test_messages.py b/tests/api_resources/chats/test_messages.py index fddd22b..1ca5b92 100644 --- a/tests/api_resources/chats/test_messages.py +++ b/tests/api_resources/chats/test_messages.py @@ -283,11 +283,6 @@ def test_method_send_with_all_params(self, client: Blooio) -> None: "url": "url", } ], - reply_to={ - "guid": "guid", - "message_id": "message_id", - "part_index": 0, - }, share_contact=True, text="string", use_typing_indicator=True, @@ -595,11 +590,6 @@ async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> N "url": "url", } ], - reply_to={ - "guid": "guid", - "message_id": "message_id", - "part_index": 0, - }, share_contact=True, text="string", use_typing_indicator=True, From 178129ec16b48bb7be95b672071147fb1a4bf679 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 21:20:25 +0000 Subject: [PATCH 04/26] feat(api): api update --- .stats.yml | 4 ++-- src/blooio/resources/chats/messages.py | 8 ++------ src/blooio/types/chats/message_react_params.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index 36f5ef8..a5d66de 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-164b95920b6f8c3bcf47d73fb582375e0e40e72a5d01a19ada82c19431b5853e.yml -openapi_spec_hash: 988aee0a657a2411a646e306ed9e2e22 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-5bfdbbd81896415e7d35d5c75de7ac794321ec5842c37fab5369f9d716837581.yml +openapi_spec_hash: ed104905ecf88a7159efed08b9680fce config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index 8f844d3..e02f570 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -209,8 +209,6 @@ def react( (-1 for last message, -2 for second-to-last, etc.). When using relative indices, you can optionally filter by message direction (inbound/outbound only). - Emoji reactions require macOS 14 (Sonoma) or later on the device. - Args: reaction: The reaction to add or remove. Must be prefixed with `+` to add or `-` to remove. @@ -220,7 +218,7 @@ def react( `-question` **Emoji reactions:** Any emoji prefixed with `+` or `-` (e.g. `+😂`, `-😂`, - `+👍`, `-🔥`). Emoji reactions require macOS 14 (Sonoma) or later on the device. + `+👍`, `-🔥`). direction: Filter by message direction (only used when messageId is a relative index like -1, -2) @@ -584,8 +582,6 @@ async def react( (-1 for last message, -2 for second-to-last, etc.). When using relative indices, you can optionally filter by message direction (inbound/outbound only). - Emoji reactions require macOS 14 (Sonoma) or later on the device. - Args: reaction: The reaction to add or remove. Must be prefixed with `+` to add or `-` to remove. @@ -595,7 +591,7 @@ async def react( `-question` **Emoji reactions:** Any emoji prefixed with `+` or `-` (e.g. `+😂`, `-😂`, - `+👍`, `-🔥`). Emoji reactions require macOS 14 (Sonoma) or later on the device. + `+👍`, `-🔥`). direction: Filter by message direction (only used when messageId is a relative index like -1, -2) diff --git a/src/blooio/types/chats/message_react_params.py b/src/blooio/types/chats/message_react_params.py index 9c6a6ed..5330e7c 100644 --- a/src/blooio/types/chats/message_react_params.py +++ b/src/blooio/types/chats/message_react_params.py @@ -22,7 +22,7 @@ class MessageReactParams(TypedDict, total=False): `-question` **Emoji reactions:** Any emoji prefixed with `+` or `-` (e.g. `+😂`, `-😂`, - `+👍`, `-🔥`). Emoji reactions require macOS 14 (Sonoma) or later on the device. + `+👍`, `-🔥`). """ direction: Literal["inbound", "outbound"] From 2e075ad61159d021fc31bd6fda9f0152ea703a34 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 21:20:28 +0000 Subject: [PATCH 05/26] feat(api): api update --- .stats.yml | 4 ++-- src/blooio/resources/chats/chats.py | 8 ++++++++ src/blooio/resources/chats/messages.py | 8 ++++++-- src/blooio/resources/me/numbers/contact_card.py | 8 ++++++++ src/blooio/types/chats/message_send_params.py | 4 +++- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index a5d66de..d38d411 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-5bfdbbd81896415e7d35d5c75de7ac794321ec5842c37fab5369f9d716837581.yml -openapi_spec_hash: ed104905ecf88a7159efed08b9680fce +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-46f59ef78fe3698c34ceea0a3ec36b2a2686471917c7c7d419eb12d128eac297.yml +openapi_spec_hash: aa44af93cee78f8800ac64bae9a578c6 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/chats.py b/src/blooio/resources/chats/chats.py index fb775fb..79cb6f8 100644 --- a/src/blooio/resources/chats/chats.py +++ b/src/blooio/resources/chats/chats.py @@ -239,6 +239,10 @@ def share_contact_card( will be piggybacked onto the next outgoing message (text or attachment) sent to this chat. This is idempotent — calling it multiple times is harmless. + ⚠️ **Plan requirement:** Contact card sharing is only available on **Dedicated + Commercial** and **Dedicated Enterprise** plans. Numbers on other plans receive + a `403`. + Args: extra_headers: Send extra headers @@ -440,6 +444,10 @@ async def share_contact_card( will be piggybacked onto the next outgoing message (text or attachment) sent to this chat. This is idempotent — calling it multiple times is harmless. + ⚠️ **Plan requirement:** Contact card sharing is only available on **Dedicated + Commercial** and **Dedicated Enterprise** plans. Numbers on other plans receive + a `403`. + Args: extra_headers: Send extra headers diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index e02f570..2a16148 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -362,7 +362,9 @@ def send( `count` instead of `message_id`. share_contact: If true, the contact card (Name & Photo) will be shared with this message. The - contact card is piggybacked onto the outgoing message. Defaults to false. + contact card is piggybacked onto the outgoing message. Defaults to false. ⚠️ + Only available on **Dedicated Commercial** and **Dedicated Enterprise** plans — + other plans receive a `403`. text: Message text. Can be a single string or array of strings (each becomes a separate message) @@ -735,7 +737,9 @@ async def send( `count` instead of `message_id`. share_contact: If true, the contact card (Name & Photo) will be shared with this message. The - contact card is piggybacked onto the outgoing message. Defaults to false. + contact card is piggybacked onto the outgoing message. Defaults to false. ⚠️ + Only available on **Dedicated Commercial** and **Dedicated Enterprise** plans — + other plans receive a `403`. text: Message text. Can be a single string or array of strings (each becomes a separate message) diff --git a/src/blooio/resources/me/numbers/contact_card.py b/src/blooio/resources/me/numbers/contact_card.py index fa47ab0..7231a43 100644 --- a/src/blooio/resources/me/numbers/contact_card.py +++ b/src/blooio/resources/me/numbers/contact_card.py @@ -100,6 +100,10 @@ def update( Update the personal contact card (Name & Photo) for the specified phone number. All fields are optional — only provided fields are updated. + ⚠️ **Plan requirement:** Setting the `first_name`, `last_name`, or `avatar` is + only available on **Dedicated Commercial** and **Dedicated Enterprise** plans. + Numbers on other plans receive a `403`. + Args: avatar: Profile photo as base64-encoded JPEG/PNG @@ -213,6 +217,10 @@ async def update( Update the personal contact card (Name & Photo) for the specified phone number. All fields are optional — only provided fields are updated. + ⚠️ **Plan requirement:** Setting the `first_name`, `last_name`, or `avatar` is + only available on **Dedicated Commercial** and **Dedicated Enterprise** plans. + Numbers on other plans receive a `403`. + Args: avatar: Profile photo as base64-encoded JPEG/PNG diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py index b2b935b..8de8730 100644 --- a/src/blooio/types/chats/message_send_params.py +++ b/src/blooio/types/chats/message_send_params.py @@ -100,7 +100,9 @@ class MessageSendParams(TypedDict, total=False): share_contact: bool """If true, the contact card (Name & Photo) will be shared with this message. - The contact card is piggybacked onto the outgoing message. Defaults to false. + The contact card is piggybacked onto the outgoing message. Defaults to false. ⚠️ + Only available on **Dedicated Commercial** and **Dedicated Enterprise** plans — + other plans receive a `403`. """ text: Union[str, SequenceNotStr[str]] From d9b82ea21659239202fb8a4d63366d54ea60fcbe Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 29 May 2026 22:20:26 +0000 Subject: [PATCH 06/26] feat(api): api update --- .stats.yml | 4 ++-- src/blooio/resources/chats/typing.py | 22 ++++++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.stats.yml b/.stats.yml index d38d411..52d7b70 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-46f59ef78fe3698c34ceea0a3ec36b2a2686471917c7c7d419eb12d128eac297.yml -openapi_spec_hash: aa44af93cee78f8800ac64bae9a578c6 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-89b373f13520ce524ae595019f2d18b83252e41615106e78b56495cb1bac6a17.yml +openapi_spec_hash: 010188e6078b32cb3e43a8f66d6090f7 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/typing.py b/src/blooio/resources/chats/typing.py index f9576e2..17a4b37 100644 --- a/src/blooio/resources/chats/typing.py +++ b/src/blooio/resources/chats/typing.py @@ -56,7 +56,9 @@ def start( """Start the typing indicator for a chat. The indicator shows the recipient that - you are typing. + you are typing. Works for both 1:1 chats (pass a phone number or email as + `chatId`) and group chats (pass the group ID, e.g. `grp_...`); in a group every + participant sees the indicator. **RCS limitation:** typing indicators are only delivered for iMessage chats — the RCS protocol does not carry composing state. Calls against RCS-routed chats @@ -92,8 +94,11 @@ def stop( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TypingResponse: - """ - Stop the typing indicator for a chat. + """Stop the typing indicator for a chat. + + Works for both 1:1 chats (pass a phone + number or email as `chatId`) and group chats (pass the group ID, e.g. + `grp_...`). **RCS limitation:** typing indicators are only delivered for iMessage chats — the RCS protocol does not carry composing state. Calls against RCS-routed chats @@ -155,7 +160,9 @@ async def start( """Start the typing indicator for a chat. The indicator shows the recipient that - you are typing. + you are typing. Works for both 1:1 chats (pass a phone number or email as + `chatId`) and group chats (pass the group ID, e.g. `grp_...`); in a group every + participant sees the indicator. **RCS limitation:** typing indicators are only delivered for iMessage chats — the RCS protocol does not carry composing state. Calls against RCS-routed chats @@ -191,8 +198,11 @@ async def stop( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TypingResponse: - """ - Stop the typing indicator for a chat. + """Stop the typing indicator for a chat. + + Works for both 1:1 chats (pass a phone + number or email as `chatId`) and group chats (pass the group ID, e.g. + `grp_...`). **RCS limitation:** typing indicators are only delivered for iMessage chats — the RCS protocol does not carry composing state. Calls against RCS-routed chats From ab48a0e1b84d2e4576714fc10688667f5841f36d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 06:20:24 +0000 Subject: [PATCH 07/26] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 52d7b70..8a65bf9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-89b373f13520ce524ae595019f2d18b83252e41615106e78b56495cb1bac6a17.yml -openapi_spec_hash: 010188e6078b32cb3e43a8f66d6090f7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-ad8942369c6c1d1ff02b4d71ab75d12c73c8895157ffc7ebde0932964b0bb3d0.yml +openapi_spec_hash: 841aacf1e549afd1218f172eca40457e config_hash: 839e191496287972d18d39dcab8b19b7 From b3ba7951d022e732ae0e3a458520ed3f42a49a5f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:20:28 +0000 Subject: [PATCH 08/26] feat(api): api update --- .stats.yml | 4 +- src/blooio/resources/chats/messages.py | 46 +++++++++++++++++++ src/blooio/types/chat_list_response.py | 6 +++ src/blooio/types/chat_retrieve_response.py | 6 +++ .../types/chats/chat_background_response.py | 8 ++++ .../types/chats/message_list_response.py | 33 ++++++++++++- .../types/chats/message_retrieve_response.py | 33 ++++++++++++- src/blooio/types/chats/message_send_params.py | 41 ++++++++++++++++- .../types/chats/message_send_response.py | 8 ++++ tests/api_resources/chats/test_messages.py | 10 ++++ 10 files changed, 190 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 8a65bf9..2d2f694 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-ad8942369c6c1d1ff02b4d71ab75d12c73c8895157ffc7ebde0932964b0bb3d0.yml -openapi_spec_hash: 841aacf1e549afd1218f172eca40457e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-11ec3fc93ebdd4d2d0699c400c1369866e3a892e8fb5270eaa409347a333b8c0.yml +openapi_spec_hash: 7461099a5a1699f3082bb99147a2399d config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index 2a16148..96dcf93 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -276,6 +276,7 @@ def send( from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, + reply_to: Optional[message_send_params.ReplyTo] | Omit = omit, share_contact: bool | Omit = omit, text: Union[str, SequenceNotStr[str]] | Omit = omit, use_typing_indicator: bool | Omit = omit, @@ -302,6 +303,20 @@ def send( message is delivered without the animation. Effects are not supported in multipart (`parts`) mode. + **Threaded replies (iMessage inline reply):** set the optional `reply_to` field + to send the outgoing message as a reply to a specific earlier message. Two + shapes are accepted: `{ "message_id": "msg_…" }` references a Blooio-minted + message in the same chat (most common — the message*id returned by an earlier + send or surfaced on a `message.received` webhook), or + `{ "guid": "…", "part_index": 0 }` references the raw iMessage GUID for the rare + case where the parent wasn't recorded by Blooio. The reply must target the same + chat and the same from-number as the new send, and the parent must be no older + than 30 days (the iMessage on-device retention horizon). Reply support is + iMessage-only and is rejected on Twilio, dashboard-Twilio, and hybrid send + paths; it's also rejected on multi-message fan-outs (`text` array or per-part + URL-balloon batch). See the `400` responses for the full set of + `reply_target*\\**` error codes. + Args: attachments: Array of attachment URLs or objects with url/name @@ -361,6 +376,13 @@ def send( `text` being a single http(s) URL. Response contains `message_ids[]` + `count` instead of `message_id`. + reply_to: Inline-reply target on `POST /chats/{chatId}/messages`. Pass either `message_id` + (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, + useful for replying to messages received before the row was minted in Blooio). + The new send is dispatched to Lava with the resolved `selectedMessageGuid` + + `partIndex`, which iMessage renders as an inline reply on the recipient's + device. + share_contact: If true, the contact card (Name & Photo) will be shared with this message. The contact card is piggybacked onto the outgoing message. Defaults to false. ⚠️ Only available on **Dedicated Commercial** and **Dedicated Enterprise** plans — @@ -391,6 +413,7 @@ def send( "from_number": from_number, "link_preview": link_preview, "parts": parts, + "reply_to": reply_to, "share_contact": share_contact, "text": text, "use_typing_indicator": use_typing_indicator, @@ -651,6 +674,7 @@ async def send( from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, + reply_to: Optional[message_send_params.ReplyTo] | Omit = omit, share_contact: bool | Omit = omit, text: Union[str, SequenceNotStr[str]] | Omit = omit, use_typing_indicator: bool | Omit = omit, @@ -677,6 +701,20 @@ async def send( message is delivered without the animation. Effects are not supported in multipart (`parts`) mode. + **Threaded replies (iMessage inline reply):** set the optional `reply_to` field + to send the outgoing message as a reply to a specific earlier message. Two + shapes are accepted: `{ "message_id": "msg_…" }` references a Blooio-minted + message in the same chat (most common — the message*id returned by an earlier + send or surfaced on a `message.received` webhook), or + `{ "guid": "…", "part_index": 0 }` references the raw iMessage GUID for the rare + case where the parent wasn't recorded by Blooio. The reply must target the same + chat and the same from-number as the new send, and the parent must be no older + than 30 days (the iMessage on-device retention horizon). Reply support is + iMessage-only and is rejected on Twilio, dashboard-Twilio, and hybrid send + paths; it's also rejected on multi-message fan-outs (`text` array or per-part + URL-balloon batch). See the `400` responses for the full set of + `reply_target*\\**` error codes. + Args: attachments: Array of attachment URLs or objects with url/name @@ -736,6 +774,13 @@ async def send( `text` being a single http(s) URL. Response contains `message_ids[]` + `count` instead of `message_id`. + reply_to: Inline-reply target on `POST /chats/{chatId}/messages`. Pass either `message_id` + (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, + useful for replying to messages received before the row was minted in Blooio). + The new send is dispatched to Lava with the resolved `selectedMessageGuid` + + `partIndex`, which iMessage renders as an inline reply on the recipient's + device. + share_contact: If true, the contact card (Name & Photo) will be shared with this message. The contact card is piggybacked onto the outgoing message. Defaults to false. ⚠️ Only available on **Dedicated Commercial** and **Dedicated Enterprise** plans — @@ -766,6 +811,7 @@ async def send( "from_number": from_number, "link_preview": link_preview, "parts": parts, + "reply_to": reply_to, "share_contact": share_contact, "text": text, "use_typing_indicator": use_typing_indicator, diff --git a/src/blooio/types/chat_list_response.py b/src/blooio/types/chat_list_response.py index 6ec4a82..ebc1194 100644 --- a/src/blooio/types/chat_list_response.py +++ b/src/blooio/types/chat_list_response.py @@ -24,6 +24,12 @@ class Chat(BaseModel): id: Optional[str] = None """Chat identifier (phone number, email, or group ID)""" + background_id: Optional[str] = None + """Identifier for the active chat background""" + + background_url: Optional[str] = None + """Public URL of the chat background image (if one has been set via the API)""" + contact: Optional[ChatContact] = None """Contact info (only for non-group chats)""" diff --git a/src/blooio/types/chat_retrieve_response.py b/src/blooio/types/chat_retrieve_response.py index 14f98a5..676d1f1 100644 --- a/src/blooio/types/chat_retrieve_response.py +++ b/src/blooio/types/chat_retrieve_response.py @@ -23,6 +23,12 @@ class ChatRetrieveResponse(BaseModel): id: Optional[str] = None """Chat identifier (phone number, email, or group ID)""" + background_id: Optional[str] = None + """Identifier for the active chat background""" + + background_url: Optional[str] = None + """Public URL of the chat background image (if one has been set via the API)""" + contact: Optional[Contact] = None """Contact info (only for non-group chats)""" diff --git a/src/blooio/types/chats/chat_background_response.py b/src/blooio/types/chats/chat_background_response.py index 48c1348..26ffacd 100644 --- a/src/blooio/types/chats/chat_background_response.py +++ b/src/blooio/types/chats/chat_background_response.py @@ -13,6 +13,14 @@ class ChatBackgroundResponse(BaseModel): background_id: Optional[str] = None """Unique identifier for the current background, or null if none""" + background_url: Optional[str] = None + """Public URL of the persisted background image stored in R2. + + Returned after a successful PUT and on GET when a background has been set + through the API. May be null if persistence failed or the background was set + outside of the API. + """ + background_version: Optional[int] = None """Version number of the background (for cache invalidation)""" diff --git a/src/blooio/types/chats/message_list_response.py b/src/blooio/types/chats/message_list_response.py index 88001a7..b31467d 100644 --- a/src/blooio/types/chats/message_list_response.py +++ b/src/blooio/types/chats/message_list_response.py @@ -7,7 +7,31 @@ from ..._models import BaseModel from ..pagination import Pagination -__all__ = ["MessageListResponse", "Message"] +__all__ = ["MessageListResponse", "Message", "MessageReplyTo"] + + +class MessageReplyTo(BaseModel): + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that returns a single message or a list of messages. + """ + + guid: Optional[str] = None + """The raw iMessage GUID of the parent. + + Always populated on real inline replies; the on-device record-of-truth + identifier that survives even when `message_id` cannot be resolved. + """ + + message_id: Optional[str] = None + """The Blooio `message_id` of the parent message. + + NULL when the parent isn't in our `messages` table (e.g., the original was sent + from outside Blooio's pipeline). + """ + + part_index: int + """Which part of the parent was replied to. 0 for the common single-part case.""" class Message(BaseModel): @@ -30,6 +54,13 @@ class Message(BaseModel): reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" + reply_to: Optional[MessageReplyTo] = None + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that + returns a single message or a list of messages. + """ + sender: Optional[str] = None """Sender's phone number or email for inbound group messages. diff --git a/src/blooio/types/chats/message_retrieve_response.py b/src/blooio/types/chats/message_retrieve_response.py index a2c61ad..aee571a 100644 --- a/src/blooio/types/chats/message_retrieve_response.py +++ b/src/blooio/types/chats/message_retrieve_response.py @@ -6,7 +6,7 @@ from .reaction import Reaction from ..._models import BaseModel -__all__ = ["MessageRetrieveResponse", "Contact"] +__all__ = ["MessageRetrieveResponse", "Contact", "ReplyTo"] class Contact(BaseModel): @@ -18,6 +18,30 @@ class Contact(BaseModel): name: Optional[str] = None +class ReplyTo(BaseModel): + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that returns a single message or a list of messages. + """ + + guid: Optional[str] = None + """The raw iMessage GUID of the parent. + + Always populated on real inline replies; the on-device record-of-truth + identifier that survives even when `message_id` cannot be resolved. + """ + + message_id: Optional[str] = None + """The Blooio `message_id` of the parent message. + + NULL when the parent isn't in our `messages` table (e.g., the original was sent + from outside Blooio's pipeline). + """ + + part_index: int + """Which part of the parent was replied to. 0 for the common single-part case.""" + + class MessageRetrieveResponse(BaseModel): attachments: Optional[List[object]] = None @@ -39,6 +63,13 @@ class MessageRetrieveResponse(BaseModel): reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" + reply_to: Optional[ReplyTo] = None + """Inline-reply parent reference. + + Identical shape on `message.received` webhooks and on every GET endpoint that + returns a single message or a list of messages. + """ + sender: Optional[str] = None """Sender's phone number or email for inbound group messages. diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py index 8de8730..84018eb 100644 --- a/src/blooio/types/chats/message_send_params.py +++ b/src/blooio/types/chats/message_send_params.py @@ -9,7 +9,7 @@ from ..._utils import PropertyInfo from .link_preview_param import LinkPreviewParam -__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionObjectVariant1", "Part"] +__all__ = ["MessageSendParams", "Attachment", "AttachmentUnionObjectVariant1", "Part", "ReplyTo"] class MessageSendParams(TypedDict, total=False): @@ -97,6 +97,16 @@ class MessageSendParams(TypedDict, total=False): `count` instead of `message_id`. """ + reply_to: Optional[ReplyTo] + """Inline-reply target on `POST /chats/{chatId}/messages`. + + Pass either `message_id` (preferred — references a Blooio-minted message) or + `guid` (raw iMessage GUID, useful for replying to messages received before the + row was minted in Blooio). The new send is dispatched to Lava with the resolved + `selectedMessageGuid` + `partIndex`, which iMessage renders as an inline reply + on the recipient's device. + """ + share_contact: bool """If true, the contact card (Name & Photo) will be shared with this message. @@ -151,3 +161,32 @@ class Part(TypedDict, total=False): url: str """URL to an attachment for this part. Mutually exclusive with 'text'.""" + + +class ReplyTo(TypedDict, total=False): + """Inline-reply target on `POST /chats/{chatId}/messages`. + + Pass either `message_id` (preferred — references a Blooio-minted message) or `guid` (raw iMessage GUID, useful for replying to messages received before the row was minted in Blooio). The new send is dispatched to Lava with the resolved `selectedMessageGuid` + `partIndex`, which iMessage renders as an inline reply on the recipient's device. + """ + + guid: str + """Raw iMessage GUID of the parent. + + When supplied without a `message_id`, Blooio attempts to look up the parent via + `provider_message_guid`; if the parent isn't in our table the send still + proceeds (Lava will thread on the device when possible) and the response carries + `parent_unresolved: true`. + """ + + message_id: str + """Blooio `message_id` of the parent. + + Must belong to the same chat, same from-number, and be no older than 30 days. + Returns 404 `reply_target_not_found` if unknown. + """ + + part_index: int + """Which part of the parent to reply to. + + Defaults to 0 (covers the 99% case of replying to a single-part text message). + """ diff --git a/src/blooio/types/chats/message_send_response.py b/src/blooio/types/chats/message_send_response.py index 385d693..04f8395 100644 --- a/src/blooio/types/chats/message_send_response.py +++ b/src/blooio/types/chats/message_send_response.py @@ -30,6 +30,14 @@ class MessageSendResponse(BaseModel): (URL-balloon batch mode). """ + parent_unresolved: Optional[bool] = None + """ + Present (and `true`) only when `reply_to.guid` was supplied without a + `message_id` and the GUID didn't map to any Blooio-minted row. The send still + proceeds and the device may still thread it; this flag signals that Blooio + couldn't link the new message to a known parent. + """ + participants: Optional[List[str]] = None """List of participants (present for multi-recipient)""" diff --git a/tests/api_resources/chats/test_messages.py b/tests/api_resources/chats/test_messages.py index 1ca5b92..fddd22b 100644 --- a/tests/api_resources/chats/test_messages.py +++ b/tests/api_resources/chats/test_messages.py @@ -283,6 +283,11 @@ def test_method_send_with_all_params(self, client: Blooio) -> None: "url": "url", } ], + reply_to={ + "guid": "guid", + "message_id": "message_id", + "part_index": 0, + }, share_contact=True, text="string", use_typing_indicator=True, @@ -590,6 +595,11 @@ async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> N "url": "url", } ], + reply_to={ + "guid": "guid", + "message_id": "message_id", + "part_index": 0, + }, share_contact=True, text="string", use_typing_indicator=True, From d692d48a74bebaa8b86c6e645941c85ebab88c2e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 02:17:59 +0000 Subject: [PATCH 09/26] fix(auth): prioritize first auth header --- src/blooio/_client.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/blooio/_client.py b/src/blooio/_client.py index 9557526..5b0f012 100644 --- a/src/blooio/_client.py +++ b/src/blooio/_client.py @@ -185,9 +185,11 @@ def qs(self) -> Querystring: @override def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: - return { - **(self._bearer_auth if security.get("bearer_auth", False) else {}), - } + headers: dict[str, str] = {} + if security.get("bearer_auth", False): + for key, value in self._bearer_auth.items(): + headers.setdefault(key, value) + return headers @property def _bearer_auth(self) -> dict[str, str]: @@ -424,9 +426,11 @@ def qs(self) -> Querystring: @override def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: - return { - **(self._bearer_auth if security.get("bearer_auth", False) else {}), - } + headers: dict[str, str] = {} + if security.get("bearer_auth", False): + for key, value in self._bearer_auth.items(): + headers.setdefault(key, value) + return headers @property def _bearer_auth(self) -> dict[str, str]: From f6caf8e2ab1630818c57ab8142a5107cc534c57c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 08:20:28 +0000 Subject: [PATCH 10/26] feat(api): api update --- .stats.yml | 4 ++-- src/blooio/resources/chats/background.py | 4 ++-- src/blooio/resources/chats/chats.py | 16 ++++++++++++---- src/blooio/resources/chats/messages.py | 16 ++++++++++++---- src/blooio/resources/contacts/contacts.py | 16 ++++++++++++---- src/blooio/resources/groups/groups.py | 16 ++++++++++++---- src/blooio/resources/groups/members.py | 16 ++++++++++++---- src/blooio/resources/webhooks/logs.py | 16 ++++++++++++---- src/blooio/types/chat_list_params.py | 12 ++++++++++-- src/blooio/types/chats/message_list_params.py | 12 ++++++++++-- src/blooio/types/contact_list_params.py | 12 ++++++++++-- src/blooio/types/group_list_params.py | 12 ++++++++++-- src/blooio/types/groups/member_list_params.py | 12 ++++++++++-- src/blooio/types/webhooks/log_list_params.py | 12 ++++++++++-- 14 files changed, 136 insertions(+), 40 deletions(-) diff --git a/.stats.yml b/.stats.yml index 2d2f694..29f6b09 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-11ec3fc93ebdd4d2d0699c400c1369866e3a892e8fb5270eaa409347a333b8c0.yml -openapi_spec_hash: 7461099a5a1699f3082bb99147a2399d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-fbf531ca47a2361a9adec0b9845e3569e800f7add519b96776c047cc227e90cc.yml +openapi_spec_hash: 636c8fd0d0264e47b02eecb86fc9b22d config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/background.py b/src/blooio/resources/chats/background.py index 52fa8ba..e76ec76 100644 --- a/src/blooio/resources/chats/background.py +++ b/src/blooio/resources/chats/background.py @@ -140,7 +140,7 @@ def set( from disk: ```bash - curl -X PUT "https://backend.blooio.com/v2/api/chats/%2B15551234567/background" \\ + curl -X PUT "https://api.blooio.com/v2/api/chats/%2B15551234567/background" \\ -H "Authorization: Bearer YOUR_API_KEY" \\ -F "background=@/path/to/image.jpg;type=image/jpeg" ``` @@ -297,7 +297,7 @@ async def set( from disk: ```bash - curl -X PUT "https://backend.blooio.com/v2/api/chats/%2B15551234567/background" \\ + curl -X PUT "https://api.blooio.com/v2/api/chats/%2B15551234567/background" \\ -H "Authorization: Bearer YOUR_API_KEY" \\ -F "background=@/path/to/image.jpg;type=image/jpeg" ``` diff --git a/src/blooio/resources/chats/chats.py b/src/blooio/resources/chats/chats.py index 79cb6f8..5c77eec 100644 --- a/src/blooio/resources/chats/chats.py +++ b/src/blooio/resources/chats/chats.py @@ -152,9 +152,13 @@ def list( message. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches phone/email or contact name) @@ -357,9 +361,13 @@ async def list( message. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches phone/email or contact name) diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index 96dcf93..c60585e 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -108,9 +108,13 @@ def list( Args: direction: Filter by message direction - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. since: Only messages sent after this timestamp (ms) @@ -506,9 +510,13 @@ async def list( Args: direction: Filter by message direction - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. since: Only messages sent after this timestamp (ms) diff --git a/src/blooio/resources/contacts/contacts.py b/src/blooio/resources/contacts/contacts.py index 3eed99f..12b2c26 100644 --- a/src/blooio/resources/contacts/contacts.py +++ b/src/blooio/resources/contacts/contacts.py @@ -193,9 +193,13 @@ def list( List all contacts for the organization with optional search and pagination. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches identifier or name) @@ -454,9 +458,13 @@ async def list( List all contacts for the organization with optional search and pagination. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches identifier or name) diff --git a/src/blooio/resources/groups/groups.py b/src/blooio/resources/groups/groups.py index 5eb4504..bb993e8 100644 --- a/src/blooio/resources/groups/groups.py +++ b/src/blooio/resources/groups/groups.py @@ -228,9 +228,13 @@ def list( List all groups for the organization with optional search and pagination. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches group name) @@ -485,9 +489,13 @@ async def list( List all groups for the organization with optional search and pagination. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. q: Search query (matches group name) diff --git a/src/blooio/resources/groups/members.py b/src/blooio/resources/groups/members.py index c14ef2b..e02cd26 100644 --- a/src/blooio/resources/groups/members.py +++ b/src/blooio/resources/groups/members.py @@ -62,9 +62,13 @@ def list( List all members of a group. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. extra_headers: Send extra headers @@ -216,9 +220,13 @@ async def list( List all members of a group. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. extra_headers: Send extra headers diff --git a/src/blooio/resources/webhooks/logs.py b/src/blooio/resources/webhooks/logs.py index c45621e..69e8f89 100644 --- a/src/blooio/resources/webhooks/logs.py +++ b/src/blooio/resources/webhooks/logs.py @@ -67,13 +67,17 @@ def list( List delivery logs for a specific webhook. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. max_status: Maximum HTTP status code min_status: Minimum HTTP status code - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. sort: Sort order by attempted time @@ -191,13 +195,17 @@ async def list( List delivery logs for a specific webhook. Args: - limit: Maximum number of items to return (1-200) + limit: Maximum number of items to return in a single response. Must be between 1 and + 200; defaults to 50. Use together with `offset` to page through large result + sets. max_status: Maximum HTTP status code min_status: Minimum HTTP status code - offset: Number of items to skip + offset: Number of items to skip before returning results. Combine with `limit` for + page-based pagination (e.g. `offset=50&limit=50` returns the second page). + Defaults to 0. sort: Sort order by attempted time diff --git a/src/blooio/types/chat_list_params.py b/src/blooio/types/chat_list_params.py index 3a6dbab..c51e343 100644 --- a/src/blooio/types/chat_list_params.py +++ b/src/blooio/types/chat_list_params.py @@ -9,10 +9,18 @@ class ChatListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ q: str """Search query (matches phone/email or contact name)""" diff --git a/src/blooio/types/chats/message_list_params.py b/src/blooio/types/chats/message_list_params.py index 0e35f39..6b2ff85 100644 --- a/src/blooio/types/chats/message_list_params.py +++ b/src/blooio/types/chats/message_list_params.py @@ -12,10 +12,18 @@ class MessageListParams(TypedDict, total=False): """Filter by message direction""" limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ since: int """Only messages sent after this timestamp (ms)""" diff --git a/src/blooio/types/contact_list_params.py b/src/blooio/types/contact_list_params.py index 6c109ff..124dfe3 100644 --- a/src/blooio/types/contact_list_params.py +++ b/src/blooio/types/contact_list_params.py @@ -9,10 +9,18 @@ class ContactListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ q: str """Search query (matches identifier or name)""" diff --git a/src/blooio/types/group_list_params.py b/src/blooio/types/group_list_params.py index 7628e4e..2bb759d 100644 --- a/src/blooio/types/group_list_params.py +++ b/src/blooio/types/group_list_params.py @@ -9,10 +9,18 @@ class GroupListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ q: str """Search query (matches group name)""" diff --git a/src/blooio/types/groups/member_list_params.py b/src/blooio/types/groups/member_list_params.py index 75b4ade..ed6fb90 100644 --- a/src/blooio/types/groups/member_list_params.py +++ b/src/blooio/types/groups/member_list_params.py @@ -9,7 +9,15 @@ class MemberListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ diff --git a/src/blooio/types/webhooks/log_list_params.py b/src/blooio/types/webhooks/log_list_params.py index d8aeaaa..c2eda44 100644 --- a/src/blooio/types/webhooks/log_list_params.py +++ b/src/blooio/types/webhooks/log_list_params.py @@ -9,7 +9,11 @@ class LogListParams(TypedDict, total=False): limit: int - """Maximum number of items to return (1-200)""" + """Maximum number of items to return in a single response. + + Must be between 1 and 200; defaults to 50. Use together with `offset` to page + through large result sets. + """ max_status: int """Maximum HTTP status code""" @@ -18,7 +22,11 @@ class LogListParams(TypedDict, total=False): """Minimum HTTP status code""" offset: int - """Number of items to skip""" + """Number of items to skip before returning results. + + Combine with `limit` for page-based pagination (e.g. `offset=50&limit=50` + returns the second page). Defaults to 0. + """ sort: Literal["asc", "desc"] """Sort order by attempted time""" From 12fb122bc963ca30ba0d2767352d2f3fde733bfb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:08:43 +0000 Subject: [PATCH 11/26] fix(internal): resolve build failures --- scripts/lint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/lint b/scripts/lint index dea1dbb..ce63bbb 100755 --- a/scripts/lint +++ b/scripts/lint @@ -13,7 +13,7 @@ else fi echo "==> Running pyright" -uv run pyright +uv run pyright -p . echo "==> Running mypy" uv run mypy . From 30e8dd13b8e2ca5988573a2cc482c122ea1650ae Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:20:29 +0000 Subject: [PATCH 12/26] feat(api): api update --- .stats.yml | 4 ++-- .../chats/message_get_status_response.py | 20 ++++++++++++++++++- .../types/chats/message_list_response.py | 20 ++++++++++++++++++- .../types/chats/message_retrieve_response.py | 20 ++++++++++++++++++- .../types/chats/message_send_response.py | 7 ++++++- .../types/webhooks/log_list_response.py | 20 ++++++++++++++++--- 6 files changed, 82 insertions(+), 9 deletions(-) diff --git a/.stats.yml b/.stats.yml index 29f6b09..4573161 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-fbf531ca47a2361a9adec0b9845e3569e800f7add519b96776c047cc227e90cc.yml -openapi_spec_hash: 636c8fd0d0264e47b02eecb86fc9b22d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-97fe57f345af4c53c5065cac2521795c0594eacd6bd094effeef5ecf72fb4dde.yml +openapi_spec_hash: e087a558db51de03fbe6e0b2faa1d8b3 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/types/chats/message_get_status_response.py b/src/blooio/types/chats/message_get_status_response.py index da637b4..669d08f 100644 --- a/src/blooio/types/chats/message_get_status_response.py +++ b/src/blooio/types/chats/message_get_status_response.py @@ -17,11 +17,29 @@ class MessageGetStatusResponse(BaseModel): message_id: Optional[str] = None - protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + protocol: Optional[Literal["pending", "unknown", "imessage", "sms", "rcs"]] = None + """Transport used to carry the message; never null. + + `pending` = accepted and dispatched, wire service not resolved yet (settles + within seconds of send); `imessage` = delivered over iMessage (blue bubble); + `rcs` = delivered over RCS; `sms` = fell back to SMS/MMS (green bubble); + `unknown` = accepted by the carrier but the wire service could not be resolved + before the tracking window closed (see `error`). + """ status: Optional[ Literal["pending", "queued", "sent", "delivered", "failed", "cancellation_requested", "cancelled"] ] = None + """Delivery lifecycle state. + + `pending` = persisted and being prepared for dispatch; `queued` = accepted and + waiting to be handed to Apple/the carrier; `sent` = handed off to Apple/the + carrier (protocol resolution happens around here); `delivered` = a delivery + receipt was received; `failed` = could not be delivered (see `error`); + `cancellation_requested` = a cancel was requested for a still-queued message + (best-effort); `cancelled` = cancelled before dispatch. Inbound messages are + surfaced via webhooks with `received`; read receipts arrive as a `read` event. + """ time_delivered: Optional[int] = None diff --git a/src/blooio/types/chats/message_list_response.py b/src/blooio/types/chats/message_list_response.py index b31467d..c24c43c 100644 --- a/src/blooio/types/chats/message_list_response.py +++ b/src/blooio/types/chats/message_list_response.py @@ -49,7 +49,15 @@ class Message(BaseModel): message_id: Optional[str] = None - protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + protocol: Optional[Literal["pending", "unknown", "imessage", "sms", "rcs"]] = None + """Transport used to carry the message; never null. + + `pending` = accepted and dispatched, wire service not resolved yet (settles + within seconds of send); `imessage` = delivered over iMessage (blue bubble); + `rcs` = delivered over RCS; `sms` = fell back to SMS/MMS (green bubble); + `unknown` = accepted by the carrier but the wire service could not be resolved + before the tracking window closed (see `error`). + """ reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" @@ -70,6 +78,16 @@ class Message(BaseModel): status: Optional[ Literal["pending", "queued", "sent", "delivered", "failed", "cancellation_requested", "cancelled"] ] = None + """Delivery lifecycle state. + + `pending` = persisted and being prepared for dispatch; `queued` = accepted and + waiting to be handed to Apple/the carrier; `sent` = handed off to Apple/the + carrier (protocol resolution happens around here); `delivered` = a delivery + receipt was received; `failed` = could not be delivered (see `error`); + `cancellation_requested` = a cancel was requested for a still-queued message + (best-effort); `cancelled` = cancelled before dispatch. Inbound messages are + surfaced via webhooks with `received`; read receipts arrive as a `read` event. + """ text: Optional[str] = None diff --git a/src/blooio/types/chats/message_retrieve_response.py b/src/blooio/types/chats/message_retrieve_response.py index aee571a..384d78b 100644 --- a/src/blooio/types/chats/message_retrieve_response.py +++ b/src/blooio/types/chats/message_retrieve_response.py @@ -58,7 +58,15 @@ class MessageRetrieveResponse(BaseModel): message_id: Optional[str] = None - protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None + protocol: Optional[Literal["pending", "unknown", "imessage", "sms", "rcs"]] = None + """Transport used to carry the message; never null. + + `pending` = accepted and dispatched, wire service not resolved yet (settles + within seconds of send); `imessage` = delivered over iMessage (blue bubble); + `rcs` = delivered over RCS; `sms` = fell back to SMS/MMS (green bubble); + `unknown` = accepted by the carrier but the wire service could not be resolved + before the tracking window closed (see `error`). + """ reactions: Optional[List[Reaction]] = None """Reactions on this message (tapbacks and emoji reactions)""" @@ -79,6 +87,16 @@ class MessageRetrieveResponse(BaseModel): status: Optional[ Literal["pending", "queued", "sent", "delivered", "failed", "cancellation_requested", "cancelled"] ] = None + """Delivery lifecycle state. + + `pending` = persisted and being prepared for dispatch; `queued` = accepted and + waiting to be handed to Apple/the carrier; `sent` = handed off to Apple/the + carrier (protocol resolution happens around here); `delivered` = a delivery + receipt was received; `failed` = could not be delivered (see `error`); + `cancellation_requested` = a cancel was requested for a still-queued message + (best-effort); `cancelled` = cancelled before dispatch. Inbound messages are + surfaced via webhooks with `received`; read receipts arrive as a `read` event. + """ text: Optional[str] = None diff --git a/src/blooio/types/chats/message_send_response.py b/src/blooio/types/chats/message_send_response.py index 04f8395..13efb90 100644 --- a/src/blooio/types/chats/message_send_response.py +++ b/src/blooio/types/chats/message_send_response.py @@ -42,4 +42,9 @@ class MessageSendResponse(BaseModel): """List of participants (present for multi-recipient)""" status: Optional[Literal["queued", "failed"]] = None - """Initial status of the message(s)""" + """Initial status of the message(s). + + `queued` = accepted for delivery (the normal 202 result); `failed` = rejected + before dispatch. Subsequent transitions (`sent` → `delivered`, or `failed`) are + reported via the status endpoint and `message.status` webhooks. + """ diff --git a/src/blooio/types/webhooks/log_list_response.py b/src/blooio/types/webhooks/log_list_response.py index 386c9d6..a85d793 100644 --- a/src/blooio/types/webhooks/log_list_response.py +++ b/src/blooio/types/webhooks/log_list_response.py @@ -75,8 +75,15 @@ class LogEventBody(BaseModel): participants: Optional[List[LogEventBodyParticipant]] = None """Array of group participants (only present when is_group=true)""" - protocol: Optional[Literal["imessage", "sms", "rcs", "non-imessage"]] = None - """Message protocol""" + protocol: Optional[Literal["pending", "unknown", "imessage", "sms", "rcs"]] = None + """Transport used to carry the message; never null. + + `pending` = accepted and dispatched, wire service not resolved yet (settles + within seconds of send); `imessage` = delivered over iMessage (blue bubble); + `rcs` = delivered over RCS; `sms` = fell back to SMS/MMS (green bubble); + `unknown` = accepted by the carrier but the wire service could not be resolved + before the tracking window closed (see `error`). + """ read_at: Optional[int] = None """Timestamp when message was read (for message.read events)""" @@ -88,7 +95,14 @@ class LogEventBody(BaseModel): """Timestamp when message was sent (for message.sent events)""" status: Optional[Literal["queued", "pending", "sent", "delivered", "failed", "read", "received"]] = None - """Message status""" + """Message status carried by the event. + + `queued` / `pending` = accepted, not yet handed off; `sent` = handed to + Apple/the carrier; `delivered` = a delivery receipt was received; `read` = a + read receipt was received (iMessage, when the recipient has read receipts on); + `failed` = delivery failed (see `error_code` / `error_message`); `received` = an + inbound message arrived. + """ text: Optional[str] = None """Message text content""" From 33ac236b6e204193681f2db4822517a0de75b182 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 02:09:39 +0000 Subject: [PATCH 13/26] feat(stlc): configurable CI runner and private-production-repo support in workflow templates --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e3308d..1baf5c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -41,7 +41,7 @@ jobs: permissions: contents: read id-token: write - runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -78,7 +78,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ${{ github.repository == 'stainless-sdks/blooio-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 From b52b04046662a8d6d49ec97f8be23b00810a9d1f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 18:20:29 +0000 Subject: [PATCH 14/26] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4573161..9fefe1e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-97fe57f345af4c53c5065cac2521795c0594eacd6bd094effeef5ecf72fb4dde.yml -openapi_spec_hash: e087a558db51de03fbe6e0b2faa1d8b3 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-48672bb20ddabae9ba6fe497480d26fd4c03079b27e0cb93c3f0decd773bbebc.yml +openapi_spec_hash: 3064147b2273936d7a3922f6778c64f7 config_hash: 839e191496287972d18d39dcab8b19b7 From 2d3528b6d08de32f67146eafdf965a21557aceeb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 08:20:30 +0000 Subject: [PATCH 15/26] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9fefe1e..4236de9 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-48672bb20ddabae9ba6fe497480d26fd4c03079b27e0cb93c3f0decd773bbebc.yml -openapi_spec_hash: 3064147b2273936d7a3922f6778c64f7 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-1cd5a817a6e1e1b6fcd4a9775ce688843f231161a23ad60d545e97c60df304b6.yml +openapi_spec_hash: 115398aaf337063929c7d212deaaa730 config_hash: 839e191496287972d18d39dcab8b19b7 From 2530e21813960ee0406f9cc30bead171daad693c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:20:25 +0000 Subject: [PATCH 16/26] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4236de9..3b51066 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-1cd5a817a6e1e1b6fcd4a9775ce688843f231161a23ad60d545e97c60df304b6.yml -openapi_spec_hash: 115398aaf337063929c7d212deaaa730 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-c60428133ba2fc28b472f6f628637fd0acba87e0213bbb85a0e8bf8c1ef3ba79.yml +openapi_spec_hash: 51ceab56aca9165d221f1294b27c140e config_hash: 839e191496287972d18d39dcab8b19b7 From 19df5720c1b7945f41e813be1d3a5bebe51e5cbd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:20:29 +0000 Subject: [PATCH 17/26] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 3b51066..fa485b0 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-c60428133ba2fc28b472f6f628637fd0acba87e0213bbb85a0e8bf8c1ef3ba79.yml -openapi_spec_hash: 51ceab56aca9165d221f1294b27c140e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-4cfd8aadba2015ef9746f076452ef853f194ce40602854c29ce76842eff021a9.yml +openapi_spec_hash: 8137fd23f3a30da82f83a8ac74a433f8 config_hash: 839e191496287972d18d39dcab8b19b7 From 9c8381ae37579567ccdb12a07440218e0c20861e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:20:26 +0000 Subject: [PATCH 18/26] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index fa485b0..061b3eb 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-4cfd8aadba2015ef9746f076452ef853f194ce40602854c29ce76842eff021a9.yml -openapi_spec_hash: 8137fd23f3a30da82f83a8ac74a433f8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-9c4d71e56301cd476e7de60e678e8ea3411448533a9542200fb769bf8c84670c.yml +openapi_spec_hash: cf6ef7c0f14168654f1cb7aba888140b config_hash: 839e191496287972d18d39dcab8b19b7 From ef1df408e0298e430c14a26942564e5a77aa259c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:20:33 +0000 Subject: [PATCH 19/26] feat(api): api update --- .stats.yml | 4 +- src/blooio/resources/webhooks/webhooks.py | 63 ++++++++++------- src/blooio/types/webhook_create_params.py | 12 ++-- tests/api_resources/test_webhooks.py | 86 +++++++++++++---------- 4 files changed, 96 insertions(+), 69 deletions(-) diff --git a/.stats.yml b/.stats.yml index 061b3eb..1a92f06 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-9c4d71e56301cd476e7de60e678e8ea3411448533a9542200fb769bf8c84670c.yml -openapi_spec_hash: cf6ef7c0f14168654f1cb7aba888140b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-5badaab948439e69a0a05e0139fc6a744ef0bb036dcf19969932fdb4c291e95b.yml +openapi_spec_hash: f9cf36b838bd4ecd09523492a4baa213 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/webhooks/webhooks.py b/src/blooio/resources/webhooks/webhooks.py index 471a3a5..82431d5 100644 --- a/src/blooio/resources/webhooks/webhooks.py +++ b/src/blooio/resources/webhooks/webhooks.py @@ -2,6 +2,7 @@ from __future__ import annotations +import typing_extensions from typing_extensions import Literal import httpx @@ -74,12 +75,12 @@ def with_streaming_response(self) -> WebhooksResourceWithStreamingResponse: """ return WebhooksResourceWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") def create( self, *, webhook_url: str, valid_until: int | Omit = omit, - webhook_type: Literal["message", "status", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -87,15 +88,19 @@ def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WebhookCreateResponse: - """ - Create a new webhook subscription. + """Registration through this endpoint is closed and returns 410. - Args: - webhook_url: URL to receive webhook events + Use POST + /v4/webhooks to create new subscriptions. Existing webhooks keep working and can + still be listed, updated, and deleted here. Re-posting the URL of a webhook that + already exists still returns 200 with that webhook, so idempotent provisioning + scripts continue to work unchanged. - valid_until: Expiration timestamp (-1 for no expiration) + Args: + webhook_url: URL of an existing webhook, for the idempotent 200 response. A URL that does not + already exist returns 410. - webhook_type: Type of events to receive + valid_until: Ignored. Retained so existing request bodies stay valid. extra_headers: Send extra headers @@ -111,7 +116,6 @@ def create( { "webhook_url": webhook_url, "valid_until": valid_until, - "webhook_type": webhook_type, }, webhook_create_params.WebhookCreateParams, ), @@ -289,12 +293,12 @@ def with_streaming_response(self) -> AsyncWebhooksResourceWithStreamingResponse: """ return AsyncWebhooksResourceWithStreamingResponse(self) + @typing_extensions.deprecated("deprecated") async def create( self, *, webhook_url: str, valid_until: int | Omit = omit, - webhook_type: Literal["message", "status", "all"] | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, @@ -302,15 +306,19 @@ async def create( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> WebhookCreateResponse: - """ - Create a new webhook subscription. + """Registration through this endpoint is closed and returns 410. - Args: - webhook_url: URL to receive webhook events + Use POST + /v4/webhooks to create new subscriptions. Existing webhooks keep working and can + still be listed, updated, and deleted here. Re-posting the URL of a webhook that + already exists still returns 200 with that webhook, so idempotent provisioning + scripts continue to work unchanged. - valid_until: Expiration timestamp (-1 for no expiration) + Args: + webhook_url: URL of an existing webhook, for the idempotent 200 response. A URL that does not + already exist returns 410. - webhook_type: Type of events to receive + valid_until: Ignored. Retained so existing request bodies stay valid. extra_headers: Send extra headers @@ -326,7 +334,6 @@ async def create( { "webhook_url": webhook_url, "valid_until": valid_until, - "webhook_type": webhook_type, }, webhook_create_params.WebhookCreateParams, ), @@ -476,8 +483,10 @@ class WebhooksResourceWithRawResponse: def __init__(self, webhooks: WebhooksResource) -> None: self._webhooks = webhooks - self.create = to_raw_response_wrapper( - webhooks.create, + self.create = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + webhooks.create, # pyright: ignore[reportDeprecated], + ) ) self.retrieve = to_raw_response_wrapper( webhooks.retrieve, @@ -507,8 +516,10 @@ class AsyncWebhooksResourceWithRawResponse: def __init__(self, webhooks: AsyncWebhooksResource) -> None: self._webhooks = webhooks - self.create = async_to_raw_response_wrapper( - webhooks.create, + self.create = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + webhooks.create, # pyright: ignore[reportDeprecated], + ) ) self.retrieve = async_to_raw_response_wrapper( webhooks.retrieve, @@ -538,8 +549,10 @@ class WebhooksResourceWithStreamingResponse: def __init__(self, webhooks: WebhooksResource) -> None: self._webhooks = webhooks - self.create = to_streamed_response_wrapper( - webhooks.create, + self.create = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + webhooks.create, # pyright: ignore[reportDeprecated], + ) ) self.retrieve = to_streamed_response_wrapper( webhooks.retrieve, @@ -569,8 +582,10 @@ class AsyncWebhooksResourceWithStreamingResponse: def __init__(self, webhooks: AsyncWebhooksResource) -> None: self._webhooks = webhooks - self.create = async_to_streamed_response_wrapper( - webhooks.create, + self.create = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + webhooks.create, # pyright: ignore[reportDeprecated], + ) ) self.retrieve = async_to_streamed_response_wrapper( webhooks.retrieve, diff --git a/src/blooio/types/webhook_create_params.py b/src/blooio/types/webhook_create_params.py index 99c36b1..2ee6ba3 100644 --- a/src/blooio/types/webhook_create_params.py +++ b/src/blooio/types/webhook_create_params.py @@ -2,17 +2,17 @@ from __future__ import annotations -from typing_extensions import Literal, Required, TypedDict +from typing_extensions import Required, TypedDict __all__ = ["WebhookCreateParams"] class WebhookCreateParams(TypedDict, total=False): webhook_url: Required[str] - """URL to receive webhook events""" + """URL of an existing webhook, for the idempotent 200 response. - valid_until: int - """Expiration timestamp (-1 for no expiration)""" + A URL that does not already exist returns 410. + """ - webhook_type: Literal["message", "status", "all"] - """Type of events to receive""" + valid_until: int + """Ignored. Retained so existing request bodies stay valid.""" diff --git a/tests/api_resources/test_webhooks.py b/tests/api_resources/test_webhooks.py index a6595cc..92c3e78 100644 --- a/tests/api_resources/test_webhooks.py +++ b/tests/api_resources/test_webhooks.py @@ -16,6 +16,8 @@ WebhookDeleteResponse, ) +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -25,27 +27,31 @@ class TestWebhooks: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create(self, client: Blooio) -> None: - webhook = client.webhooks.create( - webhook_url="https://example.com/webhook", - ) + with pytest.warns(DeprecationWarning): + webhook = client.webhooks.create( + webhook_url="https://example.com/webhook", + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_create_with_all_params(self, client: Blooio) -> None: - webhook = client.webhooks.create( - webhook_url="https://example.com/webhook", - valid_until=0, - webhook_type="message", - ) + with pytest.warns(DeprecationWarning): + webhook = client.webhooks.create( + webhook_url="https://example.com/webhook", + valid_until=0, + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_create(self, client: Blooio) -> None: - response = client.webhooks.with_raw_response.create( - webhook_url="https://example.com/webhook", - ) + with pytest.warns(DeprecationWarning): + response = client.webhooks.with_raw_response.create( + webhook_url="https://example.com/webhook", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -55,14 +61,15 @@ def test_raw_response_create(self, client: Blooio) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_streaming_response_create(self, client: Blooio) -> None: - with client.webhooks.with_streaming_response.create( - webhook_url="https://example.com/webhook", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.webhooks.with_streaming_response.create( + webhook_url="https://example.com/webhook", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - webhook = response.parse() - assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + webhook = response.parse() + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) assert cast(Any, response.is_closed) is True @@ -240,27 +247,31 @@ class TestAsyncWebhooks: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create(self, async_client: AsyncBlooio) -> None: - webhook = await async_client.webhooks.create( - webhook_url="https://example.com/webhook", - ) + with pytest.warns(DeprecationWarning): + webhook = await async_client.webhooks.create( + webhook_url="https://example.com/webhook", + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_create_with_all_params(self, async_client: AsyncBlooio) -> None: - webhook = await async_client.webhooks.create( - webhook_url="https://example.com/webhook", - valid_until=0, - webhook_type="message", - ) + with pytest.warns(DeprecationWarning): + webhook = await async_client.webhooks.create( + webhook_url="https://example.com/webhook", + valid_until=0, + ) + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_create(self, async_client: AsyncBlooio) -> None: - response = await async_client.webhooks.with_raw_response.create( - webhook_url="https://example.com/webhook", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.webhooks.with_raw_response.create( + webhook_url="https://example.com/webhook", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -270,14 +281,15 @@ async def test_raw_response_create(self, async_client: AsyncBlooio) -> None: @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_streaming_response_create(self, async_client: AsyncBlooio) -> None: - async with async_client.webhooks.with_streaming_response.create( - webhook_url="https://example.com/webhook", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - webhook = await response.parse() - assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) + with pytest.warns(DeprecationWarning): + async with async_client.webhooks.with_streaming_response.create( + webhook_url="https://example.com/webhook", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook = await response.parse() + assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) assert cast(Any, response.is_closed) is True From f725a2a9f09573cd3f72b6d0457a9eb807f2ddd9 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:20:28 +0000 Subject: [PATCH 20/26] feat(api): api update --- .stats.yml | 4 ++-- src/blooio/resources/chats/messages.py | 16 ++++++++++++++-- src/blooio/types/chats/message_send_params.py | 9 ++++++++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.stats.yml b/.stats.yml index 1a92f06..f96a450 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-5badaab948439e69a0a05e0139fc6a744ef0bb036dcf19969932fdb4c291e95b.yml -openapi_spec_hash: f9cf36b838bd4ecd09523492a4baa213 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-b3ae2931ae0f708096f6fa2bdda5fbedf167e515a96bf7cbd78f9b0cf4644c2a.yml +openapi_spec_hash: 4f7c160816c34b653e94f2c6332c37c9 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index c60585e..aa6dde1 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -322,7 +322,13 @@ def send( `reply_target*\\**` error codes. Args: - attachments: Array of attachment URLs or objects with url/name + attachments: Array of attachment URLs or objects with url/name. + + **Voice memos:** a single audio file (`.mp3`, `.m4a`, `.wav`, `.aac`, `.opus`, + `.ogg`) is automatically sent as a voice memo (the native waveform/scrubber + bubble), not a plain audio-file attachment — no extra field is needed. A voice + memo is a standalone bubble, so it cannot be combined with `text` or any other + attachment; send the voice memo and the text as two separate messages. effect: Optional. Attach an iMessage send-with-effect to the outgoing message. @@ -724,7 +730,13 @@ async def send( `reply_target*\\**` error codes. Args: - attachments: Array of attachment URLs or objects with url/name + attachments: Array of attachment URLs or objects with url/name. + + **Voice memos:** a single audio file (`.mp3`, `.m4a`, `.wav`, `.aac`, `.opus`, + `.ogg`) is automatically sent as a voice memo (the native waveform/scrubber + bubble), not a plain audio-file attachment — no extra field is needed. A voice + memo is a standalone bubble, so it cannot be combined with `text` or any other + attachment; send the voice memo and the text as two separate messages. effect: Optional. Attach an iMessage send-with-effect to the outgoing message. diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py index 84018eb..8075848 100644 --- a/src/blooio/types/chats/message_send_params.py +++ b/src/blooio/types/chats/message_send_params.py @@ -14,7 +14,14 @@ class MessageSendParams(TypedDict, total=False): attachments: SequenceNotStr[Attachment] - """Array of attachment URLs or objects with url/name""" + """Array of attachment URLs or objects with url/name. + + **Voice memos:** a single audio file (`.mp3`, `.m4a`, `.wav`, `.aac`, `.opus`, + `.ogg`) is automatically sent as a voice memo (the native waveform/scrubber + bubble), not a plain audio-file attachment — no extra field is needed. A voice + memo is a standalone bubble, so it cannot be combined with `text` or any other + attachment; send the voice memo and the text as two separate messages. + """ effect: Optional[ Literal[ From c8e68277578cd19811353635d943a5589c705a30 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 21:20:36 +0000 Subject: [PATCH 21/26] feat(api): api update --- .stats.yml | 4 +-- api.md | 19 +++--------- src/blooio/resources/facetime.py | 13 ++++---- src/blooio/resources/groups/members.py | 24 ++++++++------- src/blooio/types/__init__.py | 1 - .../types/facetime_initiate_call_response.py | 17 ----------- src/blooio/types/groups/__init__.py | 2 -- .../types/groups/member_add_response.py | 14 --------- .../types/groups/member_remove_response.py | 13 -------- tests/api_resources/groups/test_members.py | 30 ++++++++----------- tests/api_resources/test_facetime.py | 14 ++++----- 11 files changed, 45 insertions(+), 106 deletions(-) delete mode 100644 src/blooio/types/facetime_initiate_call_response.py delete mode 100644 src/blooio/types/groups/member_add_response.py delete mode 100644 src/blooio/types/groups/member_remove_response.py diff --git a/.stats.yml b/.stats.yml index f96a450..fd5611a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-b3ae2931ae0f708096f6fa2bdda5fbedf167e515a96bf7cbd78f9b0cf4644c2a.yml -openapi_spec_hash: 4f7c160816c34b653e94f2c6332c37c9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-18850db845af2adb702c53b439259038282c2aa185d9ef128815bf04608b931f.yml +openapi_spec_hash: 789e2d6077592c6f781c67c33ea2a855 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/api.md b/api.md index f00d67d..b0be732 100644 --- a/api.md +++ b/api.md @@ -90,15 +90,9 @@ Methods: # Facetime -Types: - -```python -from blooio.types import FacetimeInitiateCallResponse -``` - Methods: -- client.facetime.initiate_call(\*\*params) -> FacetimeInitiateCallResponse +- client.facetime.initiate_call(\*\*params) -> None # Groups @@ -127,19 +121,14 @@ Methods: Types: ```python -from blooio.types.groups import ( - GroupMember, - MemberListResponse, - MemberAddResponse, - MemberRemoveResponse, -) +from blooio.types.groups import GroupMember, MemberListResponse ``` Methods: - client.groups.members.list(group_id, \*\*params) -> MemberListResponse -- client.groups.members.add(group_id, \*\*params) -> MemberAddResponse -- client.groups.members.remove(contact_id, \*, group_id) -> MemberRemoveResponse +- client.groups.members.add(group_id, \*\*params) -> None +- client.groups.members.remove(contact_id, \*, group_id) -> None ## Icon diff --git a/src/blooio/resources/facetime.py b/src/blooio/resources/facetime.py index ad8ac80..5d4528f 100644 --- a/src/blooio/resources/facetime.py +++ b/src/blooio/resources/facetime.py @@ -5,7 +5,7 @@ import httpx from ..types import facetime_initiate_call_params -from .._types import Body, Query, Headers, NotGiven, not_given +from .._types import Body, Query, Headers, NoneType, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -16,7 +16,6 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.facetime_initiate_call_response import FacetimeInitiateCallResponse __all__ = ["FacetimeResource", "AsyncFacetimeResource"] @@ -53,7 +52,7 @@ def initiate_call( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> FacetimeInitiateCallResponse: + ) -> None: """ **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the FaceTime call flow. @@ -73,13 +72,14 @@ def initiate_call( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( "/facetime/calls", body=maybe_transform({"handle": handle}, facetime_initiate_call_params.FacetimeInitiateCallParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=FacetimeInitiateCallResponse, + cast_to=NoneType, ) @@ -115,7 +115,7 @@ async def initiate_call( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> FacetimeInitiateCallResponse: + ) -> None: """ **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the FaceTime call flow. @@ -135,6 +135,7 @@ async def initiate_call( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( "/facetime/calls", body=await async_maybe_transform( @@ -143,7 +144,7 @@ async def initiate_call( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=FacetimeInitiateCallResponse, + cast_to=NoneType, ) diff --git a/src/blooio/resources/groups/members.py b/src/blooio/resources/groups/members.py index e02cd26..1c5802f 100644 --- a/src/blooio/resources/groups/members.py +++ b/src/blooio/resources/groups/members.py @@ -4,7 +4,7 @@ import httpx -from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -16,9 +16,7 @@ ) from ..._base_client import make_request_options from ...types.groups import member_add_params, member_list_params -from ...types.groups.member_add_response import MemberAddResponse from ...types.groups.member_list_response import MemberListResponse -from ...types.groups.member_remove_response import MemberRemoveResponse __all__ = ["MembersResource", "AsyncMembersResource"] @@ -109,7 +107,7 @@ def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberAddResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -130,13 +128,14 @@ def add( """ if not group_id: raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( path_template("/groups/{group_id}/members", group_id=group_id), body=maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberAddResponse, + cast_to=NoneType, ) def remove( @@ -150,7 +149,7 @@ def remove( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberRemoveResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -172,12 +171,13 @@ def remove( raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") if not contact_id: raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberRemoveResponse, + cast_to=NoneType, ) @@ -267,7 +267,7 @@ async def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberAddResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -288,13 +288,14 @@ async def add( """ if not group_id: raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( path_template("/groups/{group_id}/members", group_id=group_id), body=await async_maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberAddResponse, + cast_to=NoneType, ) async def remove( @@ -308,7 +309,7 @@ async def remove( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberRemoveResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -330,12 +331,13 @@ async def remove( raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") if not contact_id: raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberRemoveResponse, + cast_to=NoneType, ) diff --git a/src/blooio/types/__init__.py b/src/blooio/types/__init__.py index dad7cd9..b326c61 100644 --- a/src/blooio/types/__init__.py +++ b/src/blooio/types/__init__.py @@ -30,7 +30,6 @@ from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse from .chat_mark_as_read_response import ChatMarkAsReadResponse as ChatMarkAsReadResponse from .facetime_initiate_call_params import FacetimeInitiateCallParams as FacetimeInitiateCallParams -from .facetime_initiate_call_response import FacetimeInitiateCallResponse as FacetimeInitiateCallResponse from .chat_share_contact_card_response import ChatShareContactCardResponse as ChatShareContactCardResponse from .phone_number_batch_create_params import PhoneNumberBatchCreateParams as PhoneNumberBatchCreateParams from .phone_number_batch_create_response import PhoneNumberBatchCreateResponse as PhoneNumberBatchCreateResponse diff --git a/src/blooio/types/facetime_initiate_call_response.py b/src/blooio/types/facetime_initiate_call_response.py deleted file mode 100644 index 7b1d8f8..0000000 --- a/src/blooio/types/facetime_initiate_call_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["FacetimeInitiateCallResponse"] - - -class FacetimeInitiateCallResponse(BaseModel): - handle: Optional[str] = None - """The handle that was called""" - - link: Optional[str] = None - """Shareable FaceTime link""" - - success: Optional[bool] = None diff --git a/src/blooio/types/groups/__init__.py b/src/blooio/types/groups/__init__.py index caf4ab2..b1908ac 100644 --- a/src/blooio/types/groups/__init__.py +++ b/src/blooio/types/groups/__init__.py @@ -7,6 +7,4 @@ from .icon_set_params import IconSetParams as IconSetParams from .member_add_params import MemberAddParams as MemberAddParams from .member_list_params import MemberListParams as MemberListParams -from .member_add_response import MemberAddResponse as MemberAddResponse from .member_list_response import MemberListResponse as MemberListResponse -from .member_remove_response import MemberRemoveResponse as MemberRemoveResponse diff --git a/src/blooio/types/groups/member_add_response.py b/src/blooio/types/groups/member_add_response.py deleted file mode 100644 index 606e03e..0000000 --- a/src/blooio/types/groups/member_add_response.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel -from .group_member import GroupMember - -__all__ = ["MemberAddResponse"] - - -class MemberAddResponse(BaseModel): - member: Optional[GroupMember] = None - - message: Optional[str] = None diff --git a/src/blooio/types/groups/member_remove_response.py b/src/blooio/types/groups/member_remove_response.py deleted file mode 100644 index e1cfe9c..0000000 --- a/src/blooio/types/groups/member_remove_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel - -__all__ = ["MemberRemoveResponse"] - - -class MemberRemoveResponse(BaseModel): - removed_at: Optional[int] = None - - success: Optional[bool] = None diff --git a/tests/api_resources/groups/test_members.py b/tests/api_resources/groups/test_members.py index 8ccc6ba..fd7b097 100644 --- a/tests/api_resources/groups/test_members.py +++ b/tests/api_resources/groups/test_members.py @@ -9,11 +9,7 @@ from blooio import Blooio, AsyncBlooio from tests.utils import assert_matches_type -from blooio.types.groups import ( - MemberAddResponse, - MemberListResponse, - MemberRemoveResponse, -) +from blooio.types.groups import MemberListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -80,7 +76,7 @@ def test_method_add(self, client: Blooio) -> None: group_id="grp_abc123def456", contact_id="+15551234567", ) - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -93,7 +89,7 @@ def test_raw_response_add(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -106,7 +102,7 @@ def test_streaming_response_add(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True @@ -126,7 +122,7 @@ def test_method_remove(self, client: Blooio) -> None: contact_id="%2B15551234567", group_id="grp_abc123def456", ) - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -139,7 +135,7 @@ def test_raw_response_remove(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -152,7 +148,7 @@ def test_streaming_response_remove(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True @@ -236,7 +232,7 @@ async def test_method_add(self, async_client: AsyncBlooio) -> None: group_id="grp_abc123def456", contact_id="+15551234567", ) - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -249,7 +245,7 @@ async def test_raw_response_add(self, async_client: AsyncBlooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -262,7 +258,7 @@ async def test_streaming_response_add(self, async_client: AsyncBlooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True @@ -282,7 +278,7 @@ async def test_method_remove(self, async_client: AsyncBlooio) -> None: contact_id="%2B15551234567", group_id="grp_abc123def456", ) - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -295,7 +291,7 @@ async def test_raw_response_remove(self, async_client: AsyncBlooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -308,7 +304,7 @@ async def test_streaming_response_remove(self, async_client: AsyncBlooio) -> Non assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_facetime.py b/tests/api_resources/test_facetime.py index 3611d91..9d5d2cd 100644 --- a/tests/api_resources/test_facetime.py +++ b/tests/api_resources/test_facetime.py @@ -8,8 +8,6 @@ import pytest from blooio import Blooio, AsyncBlooio -from tests.utils import assert_matches_type -from blooio.types import FacetimeInitiateCallResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,7 +21,7 @@ def test_method_initiate_call(self, client: Blooio) -> None: facetime = client.facetime.initiate_call( handle="+15551234567", ) - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -35,7 +33,7 @@ def test_raw_response_initiate_call(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -47,7 +45,7 @@ def test_streaming_response_initiate_call(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None assert cast(Any, response.is_closed) is True @@ -63,7 +61,7 @@ async def test_method_initiate_call(self, async_client: AsyncBlooio) -> None: facetime = await async_client.facetime.initiate_call( handle="+15551234567", ) - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -75,7 +73,7 @@ async def test_raw_response_initiate_call(self, async_client: AsyncBlooio) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = await response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -87,6 +85,6 @@ async def test_streaming_response_initiate_call(self, async_client: AsyncBlooio) assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = await response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None assert cast(Any, response.is_closed) is True From cf471a562375341931981795ed31394d650718c0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 22:20:40 +0000 Subject: [PATCH 22/26] feat(api): api update --- .stats.yml | 4 +-- api.md | 19 +++++++++--- src/blooio/resources/facetime.py | 13 ++++---- src/blooio/resources/groups/members.py | 24 +++++++-------- src/blooio/types/__init__.py | 1 + .../types/facetime_initiate_call_response.py | 17 +++++++++++ src/blooio/types/groups/__init__.py | 2 ++ .../types/groups/member_add_response.py | 14 +++++++++ .../types/groups/member_remove_response.py | 13 ++++++++ tests/api_resources/groups/test_members.py | 30 +++++++++++-------- tests/api_resources/test_facetime.py | 14 +++++---- 11 files changed, 106 insertions(+), 45 deletions(-) create mode 100644 src/blooio/types/facetime_initiate_call_response.py create mode 100644 src/blooio/types/groups/member_add_response.py create mode 100644 src/blooio/types/groups/member_remove_response.py diff --git a/.stats.yml b/.stats.yml index fd5611a..f96a450 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-18850db845af2adb702c53b439259038282c2aa185d9ef128815bf04608b931f.yml -openapi_spec_hash: 789e2d6077592c6f781c67c33ea2a855 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-b3ae2931ae0f708096f6fa2bdda5fbedf167e515a96bf7cbd78f9b0cf4644c2a.yml +openapi_spec_hash: 4f7c160816c34b653e94f2c6332c37c9 config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/api.md b/api.md index b0be732..f00d67d 100644 --- a/api.md +++ b/api.md @@ -90,9 +90,15 @@ Methods: # Facetime +Types: + +```python +from blooio.types import FacetimeInitiateCallResponse +``` + Methods: -- client.facetime.initiate_call(\*\*params) -> None +- client.facetime.initiate_call(\*\*params) -> FacetimeInitiateCallResponse # Groups @@ -121,14 +127,19 @@ Methods: Types: ```python -from blooio.types.groups import GroupMember, MemberListResponse +from blooio.types.groups import ( + GroupMember, + MemberListResponse, + MemberAddResponse, + MemberRemoveResponse, +) ``` Methods: - client.groups.members.list(group_id, \*\*params) -> MemberListResponse -- client.groups.members.add(group_id, \*\*params) -> None -- client.groups.members.remove(contact_id, \*, group_id) -> None +- client.groups.members.add(group_id, \*\*params) -> MemberAddResponse +- client.groups.members.remove(contact_id, \*, group_id) -> MemberRemoveResponse ## Icon diff --git a/src/blooio/resources/facetime.py b/src/blooio/resources/facetime.py index 5d4528f..ad8ac80 100644 --- a/src/blooio/resources/facetime.py +++ b/src/blooio/resources/facetime.py @@ -5,7 +5,7 @@ import httpx from ..types import facetime_initiate_call_params -from .._types import Body, Query, Headers, NoneType, NotGiven, not_given +from .._types import Body, Query, Headers, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -16,6 +16,7 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options +from ..types.facetime_initiate_call_response import FacetimeInitiateCallResponse __all__ = ["FacetimeResource", "AsyncFacetimeResource"] @@ -52,7 +53,7 @@ def initiate_call( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> FacetimeInitiateCallResponse: """ **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the FaceTime call flow. @@ -72,14 +73,13 @@ def initiate_call( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( "/facetime/calls", body=maybe_transform({"handle": handle}, facetime_initiate_call_params.FacetimeInitiateCallParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=FacetimeInitiateCallResponse, ) @@ -115,7 +115,7 @@ async def initiate_call( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> FacetimeInitiateCallResponse: """ **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the FaceTime call flow. @@ -135,7 +135,6 @@ async def initiate_call( timeout: Override the client-level default timeout for this request, in seconds """ - extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( "/facetime/calls", body=await async_maybe_transform( @@ -144,7 +143,7 @@ async def initiate_call( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=FacetimeInitiateCallResponse, ) diff --git a/src/blooio/resources/groups/members.py b/src/blooio/resources/groups/members.py index 1c5802f..e02cd26 100644 --- a/src/blooio/resources/groups/members.py +++ b/src/blooio/resources/groups/members.py @@ -4,7 +4,7 @@ import httpx -from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -16,7 +16,9 @@ ) from ..._base_client import make_request_options from ...types.groups import member_add_params, member_list_params +from ...types.groups.member_add_response import MemberAddResponse from ...types.groups.member_list_response import MemberListResponse +from ...types.groups.member_remove_response import MemberRemoveResponse __all__ = ["MembersResource", "AsyncMembersResource"] @@ -107,7 +109,7 @@ def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> MemberAddResponse: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -128,14 +130,13 @@ def add( """ if not group_id: raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( path_template("/groups/{group_id}/members", group_id=group_id), body=maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=MemberAddResponse, ) def remove( @@ -149,7 +150,7 @@ def remove( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> MemberRemoveResponse: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -171,13 +172,12 @@ def remove( raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") if not contact_id: raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=MemberRemoveResponse, ) @@ -267,7 +267,7 @@ async def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> MemberAddResponse: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -288,14 +288,13 @@ async def add( """ if not group_id: raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( path_template("/groups/{group_id}/members", group_id=group_id), body=await async_maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=MemberAddResponse, ) async def remove( @@ -309,7 +308,7 @@ async def remove( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> None: + ) -> MemberRemoveResponse: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -331,13 +330,12 @@ async def remove( raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") if not contact_id: raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") - extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=NoneType, + cast_to=MemberRemoveResponse, ) diff --git a/src/blooio/types/__init__.py b/src/blooio/types/__init__.py index b326c61..dad7cd9 100644 --- a/src/blooio/types/__init__.py +++ b/src/blooio/types/__init__.py @@ -30,6 +30,7 @@ from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse from .chat_mark_as_read_response import ChatMarkAsReadResponse as ChatMarkAsReadResponse from .facetime_initiate_call_params import FacetimeInitiateCallParams as FacetimeInitiateCallParams +from .facetime_initiate_call_response import FacetimeInitiateCallResponse as FacetimeInitiateCallResponse from .chat_share_contact_card_response import ChatShareContactCardResponse as ChatShareContactCardResponse from .phone_number_batch_create_params import PhoneNumberBatchCreateParams as PhoneNumberBatchCreateParams from .phone_number_batch_create_response import PhoneNumberBatchCreateResponse as PhoneNumberBatchCreateResponse diff --git a/src/blooio/types/facetime_initiate_call_response.py b/src/blooio/types/facetime_initiate_call_response.py new file mode 100644 index 0000000..7b1d8f8 --- /dev/null +++ b/src/blooio/types/facetime_initiate_call_response.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["FacetimeInitiateCallResponse"] + + +class FacetimeInitiateCallResponse(BaseModel): + handle: Optional[str] = None + """The handle that was called""" + + link: Optional[str] = None + """Shareable FaceTime link""" + + success: Optional[bool] = None diff --git a/src/blooio/types/groups/__init__.py b/src/blooio/types/groups/__init__.py index b1908ac..caf4ab2 100644 --- a/src/blooio/types/groups/__init__.py +++ b/src/blooio/types/groups/__init__.py @@ -7,4 +7,6 @@ from .icon_set_params import IconSetParams as IconSetParams from .member_add_params import MemberAddParams as MemberAddParams from .member_list_params import MemberListParams as MemberListParams +from .member_add_response import MemberAddResponse as MemberAddResponse from .member_list_response import MemberListResponse as MemberListResponse +from .member_remove_response import MemberRemoveResponse as MemberRemoveResponse diff --git a/src/blooio/types/groups/member_add_response.py b/src/blooio/types/groups/member_add_response.py new file mode 100644 index 0000000..606e03e --- /dev/null +++ b/src/blooio/types/groups/member_add_response.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel +from .group_member import GroupMember + +__all__ = ["MemberAddResponse"] + + +class MemberAddResponse(BaseModel): + member: Optional[GroupMember] = None + + message: Optional[str] = None diff --git a/src/blooio/types/groups/member_remove_response.py b/src/blooio/types/groups/member_remove_response.py new file mode 100644 index 0000000..e1cfe9c --- /dev/null +++ b/src/blooio/types/groups/member_remove_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["MemberRemoveResponse"] + + +class MemberRemoveResponse(BaseModel): + removed_at: Optional[int] = None + + success: Optional[bool] = None diff --git a/tests/api_resources/groups/test_members.py b/tests/api_resources/groups/test_members.py index fd7b097..8ccc6ba 100644 --- a/tests/api_resources/groups/test_members.py +++ b/tests/api_resources/groups/test_members.py @@ -9,7 +9,11 @@ from blooio import Blooio, AsyncBlooio from tests.utils import assert_matches_type -from blooio.types.groups import MemberListResponse +from blooio.types.groups import ( + MemberAddResponse, + MemberListResponse, + MemberRemoveResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -76,7 +80,7 @@ def test_method_add(self, client: Blooio) -> None: group_id="grp_abc123def456", contact_id="+15551234567", ) - assert member is None + assert_matches_type(MemberAddResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -89,7 +93,7 @@ def test_raw_response_add(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert member is None + assert_matches_type(MemberAddResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -102,7 +106,7 @@ def test_streaming_response_add(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert member is None + assert_matches_type(MemberAddResponse, member, path=["response"]) assert cast(Any, response.is_closed) is True @@ -122,7 +126,7 @@ def test_method_remove(self, client: Blooio) -> None: contact_id="%2B15551234567", group_id="grp_abc123def456", ) - assert member is None + assert_matches_type(MemberRemoveResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -135,7 +139,7 @@ def test_raw_response_remove(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert member is None + assert_matches_type(MemberRemoveResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -148,7 +152,7 @@ def test_streaming_response_remove(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert member is None + assert_matches_type(MemberRemoveResponse, member, path=["response"]) assert cast(Any, response.is_closed) is True @@ -232,7 +236,7 @@ async def test_method_add(self, async_client: AsyncBlooio) -> None: group_id="grp_abc123def456", contact_id="+15551234567", ) - assert member is None + assert_matches_type(MemberAddResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -245,7 +249,7 @@ async def test_raw_response_add(self, async_client: AsyncBlooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert member is None + assert_matches_type(MemberAddResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -258,7 +262,7 @@ async def test_streaming_response_add(self, async_client: AsyncBlooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert member is None + assert_matches_type(MemberAddResponse, member, path=["response"]) assert cast(Any, response.is_closed) is True @@ -278,7 +282,7 @@ async def test_method_remove(self, async_client: AsyncBlooio) -> None: contact_id="%2B15551234567", group_id="grp_abc123def456", ) - assert member is None + assert_matches_type(MemberRemoveResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -291,7 +295,7 @@ async def test_raw_response_remove(self, async_client: AsyncBlooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert member is None + assert_matches_type(MemberRemoveResponse, member, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -304,7 +308,7 @@ async def test_streaming_response_remove(self, async_client: AsyncBlooio) -> Non assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert member is None + assert_matches_type(MemberRemoveResponse, member, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_facetime.py b/tests/api_resources/test_facetime.py index 9d5d2cd..3611d91 100644 --- a/tests/api_resources/test_facetime.py +++ b/tests/api_resources/test_facetime.py @@ -8,6 +8,8 @@ import pytest from blooio import Blooio, AsyncBlooio +from tests.utils import assert_matches_type +from blooio.types import FacetimeInitiateCallResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -21,7 +23,7 @@ def test_method_initiate_call(self, client: Blooio) -> None: facetime = client.facetime.initiate_call( handle="+15551234567", ) - assert facetime is None + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -33,7 +35,7 @@ def test_raw_response_initiate_call(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = response.parse() - assert facetime is None + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -45,7 +47,7 @@ def test_streaming_response_initiate_call(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = response.parse() - assert facetime is None + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) assert cast(Any, response.is_closed) is True @@ -61,7 +63,7 @@ async def test_method_initiate_call(self, async_client: AsyncBlooio) -> None: facetime = await async_client.facetime.initiate_call( handle="+15551234567", ) - assert facetime is None + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -73,7 +75,7 @@ async def test_raw_response_initiate_call(self, async_client: AsyncBlooio) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = await response.parse() - assert facetime is None + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -85,6 +87,6 @@ async def test_streaming_response_initiate_call(self, async_client: AsyncBlooio) assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = await response.parse() - assert facetime is None + assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) assert cast(Any, response.is_closed) is True From e4233820761194e86157825cb2fd1aa3925357da Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:20:28 +0000 Subject: [PATCH 23/26] feat(api): api update --- .stats.yml | 4 +-- api.md | 19 +++--------- src/blooio/resources/chats/messages.py | 8 +++++ src/blooio/resources/facetime.py | 13 ++++---- src/blooio/resources/groups/members.py | 24 ++++++++------- src/blooio/types/__init__.py | 1 - .../types/facetime_initiate_call_response.py | 17 ----------- src/blooio/types/groups/__init__.py | 2 -- .../types/groups/member_add_response.py | 14 --------- .../types/groups/member_remove_response.py | 13 -------- tests/api_resources/groups/test_members.py | 30 ++++++++----------- tests/api_resources/test_facetime.py | 14 ++++----- 12 files changed, 53 insertions(+), 106 deletions(-) delete mode 100644 src/blooio/types/facetime_initiate_call_response.py delete mode 100644 src/blooio/types/groups/member_add_response.py delete mode 100644 src/blooio/types/groups/member_remove_response.py diff --git a/.stats.yml b/.stats.yml index f96a450..9c9d64a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-b3ae2931ae0f708096f6fa2bdda5fbedf167e515a96bf7cbd78f9b0cf4644c2a.yml -openapi_spec_hash: 4f7c160816c34b653e94f2c6332c37c9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-14138ff0ac1c85c9b0e958c0596bd8deec484fdeadb4199d0c6c21d51b513f69.yml +openapi_spec_hash: 4814e4ad505a416e0b44b658ce7b2f5d config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/api.md b/api.md index f00d67d..b0be732 100644 --- a/api.md +++ b/api.md @@ -90,15 +90,9 @@ Methods: # Facetime -Types: - -```python -from blooio.types import FacetimeInitiateCallResponse -``` - Methods: -- client.facetime.initiate_call(\*\*params) -> FacetimeInitiateCallResponse +- client.facetime.initiate_call(\*\*params) -> None # Groups @@ -127,19 +121,14 @@ Methods: Types: ```python -from blooio.types.groups import ( - GroupMember, - MemberListResponse, - MemberAddResponse, - MemberRemoveResponse, -) +from blooio.types.groups import GroupMember, MemberListResponse ``` Methods: - client.groups.members.list(group_id, \*\*params) -> MemberListResponse -- client.groups.members.add(group_id, \*\*params) -> MemberAddResponse -- client.groups.members.remove(contact_id, \*, group_id) -> MemberRemoveResponse +- client.groups.members.add(group_id, \*\*params) -> None +- client.groups.members.remove(contact_id, \*, group_id) -> None ## Icon diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index aa6dde1..ce33663 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -105,6 +105,10 @@ def list( """ List all messages in a conversation with optional filtering. + A conversation must already exist: this returns `404` for an address the + organization has never exchanged a message with, rather than an empty list. Use + `GET /chats` to enumerate the conversations that do exist. + Args: direction: Filter by message direction @@ -513,6 +517,10 @@ async def list( """ List all messages in a conversation with optional filtering. + A conversation must already exist: this returns `404` for an address the + organization has never exchanged a message with, rather than an empty list. Use + `GET /chats` to enumerate the conversations that do exist. + Args: direction: Filter by message direction diff --git a/src/blooio/resources/facetime.py b/src/blooio/resources/facetime.py index ad8ac80..5d4528f 100644 --- a/src/blooio/resources/facetime.py +++ b/src/blooio/resources/facetime.py @@ -5,7 +5,7 @@ import httpx from ..types import facetime_initiate_call_params -from .._types import Body, Query, Headers, NotGiven, not_given +from .._types import Body, Query, Headers, NoneType, NotGiven, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource @@ -16,7 +16,6 @@ async_to_streamed_response_wrapper, ) from .._base_client import make_request_options -from ..types.facetime_initiate_call_response import FacetimeInitiateCallResponse __all__ = ["FacetimeResource", "AsyncFacetimeResource"] @@ -53,7 +52,7 @@ def initiate_call( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> FacetimeInitiateCallResponse: + ) -> None: """ **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the FaceTime call flow. @@ -73,13 +72,14 @@ def initiate_call( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( "/facetime/calls", body=maybe_transform({"handle": handle}, facetime_initiate_call_params.FacetimeInitiateCallParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=FacetimeInitiateCallResponse, + cast_to=NoneType, ) @@ -115,7 +115,7 @@ async def initiate_call( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> FacetimeInitiateCallResponse: + ) -> None: """ **Coming Soon** -- This endpoint is temporarily disabled while we stabilize the FaceTime call flow. @@ -135,6 +135,7 @@ async def initiate_call( timeout: Override the client-level default timeout for this request, in seconds """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( "/facetime/calls", body=await async_maybe_transform( @@ -143,7 +144,7 @@ async def initiate_call( options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=FacetimeInitiateCallResponse, + cast_to=NoneType, ) diff --git a/src/blooio/resources/groups/members.py b/src/blooio/resources/groups/members.py index e02cd26..1c5802f 100644 --- a/src/blooio/resources/groups/members.py +++ b/src/blooio/resources/groups/members.py @@ -4,7 +4,7 @@ import httpx -from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given from ..._utils import path_template, maybe_transform, async_maybe_transform from ..._compat import cached_property from ..._resource import SyncAPIResource, AsyncAPIResource @@ -16,9 +16,7 @@ ) from ..._base_client import make_request_options from ...types.groups import member_add_params, member_list_params -from ...types.groups.member_add_response import MemberAddResponse from ...types.groups.member_list_response import MemberListResponse -from ...types.groups.member_remove_response import MemberRemoveResponse __all__ = ["MembersResource", "AsyncMembersResource"] @@ -109,7 +107,7 @@ def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberAddResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -130,13 +128,14 @@ def add( """ if not group_id: raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._post( path_template("/groups/{group_id}/members", group_id=group_id), body=maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberAddResponse, + cast_to=NoneType, ) def remove( @@ -150,7 +149,7 @@ def remove( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberRemoveResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -172,12 +171,13 @@ def remove( raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") if not contact_id: raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return self._delete( path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberRemoveResponse, + cast_to=NoneType, ) @@ -267,7 +267,7 @@ async def add( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberAddResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -288,13 +288,14 @@ async def add( """ if not group_id: raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._post( path_template("/groups/{group_id}/members", group_id=group_id), body=await async_maybe_transform({"contact_id": contact_id}, member_add_params.MemberAddParams), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberAddResponse, + cast_to=NoneType, ) async def remove( @@ -308,7 +309,7 @@ async def remove( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> MemberRemoveResponse: + ) -> None: """ ⚠️ **COMING SOON** - This endpoint is temporarily disabled while we stabilize this feature. @@ -330,12 +331,13 @@ async def remove( raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") if not contact_id: raise ValueError(f"Expected a non-empty value for `contact_id` but received {contact_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} return await self._delete( path_template("/groups/{group_id}/members/{contact_id}", group_id=group_id, contact_id=contact_id), options=make_request_options( extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), - cast_to=MemberRemoveResponse, + cast_to=NoneType, ) diff --git a/src/blooio/types/__init__.py b/src/blooio/types/__init__.py index dad7cd9..b326c61 100644 --- a/src/blooio/types/__init__.py +++ b/src/blooio/types/__init__.py @@ -30,7 +30,6 @@ from .webhook_delete_response import WebhookDeleteResponse as WebhookDeleteResponse from .chat_mark_as_read_response import ChatMarkAsReadResponse as ChatMarkAsReadResponse from .facetime_initiate_call_params import FacetimeInitiateCallParams as FacetimeInitiateCallParams -from .facetime_initiate_call_response import FacetimeInitiateCallResponse as FacetimeInitiateCallResponse from .chat_share_contact_card_response import ChatShareContactCardResponse as ChatShareContactCardResponse from .phone_number_batch_create_params import PhoneNumberBatchCreateParams as PhoneNumberBatchCreateParams from .phone_number_batch_create_response import PhoneNumberBatchCreateResponse as PhoneNumberBatchCreateResponse diff --git a/src/blooio/types/facetime_initiate_call_response.py b/src/blooio/types/facetime_initiate_call_response.py deleted file mode 100644 index 7b1d8f8..0000000 --- a/src/blooio/types/facetime_initiate_call_response.py +++ /dev/null @@ -1,17 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from .._models import BaseModel - -__all__ = ["FacetimeInitiateCallResponse"] - - -class FacetimeInitiateCallResponse(BaseModel): - handle: Optional[str] = None - """The handle that was called""" - - link: Optional[str] = None - """Shareable FaceTime link""" - - success: Optional[bool] = None diff --git a/src/blooio/types/groups/__init__.py b/src/blooio/types/groups/__init__.py index caf4ab2..b1908ac 100644 --- a/src/blooio/types/groups/__init__.py +++ b/src/blooio/types/groups/__init__.py @@ -7,6 +7,4 @@ from .icon_set_params import IconSetParams as IconSetParams from .member_add_params import MemberAddParams as MemberAddParams from .member_list_params import MemberListParams as MemberListParams -from .member_add_response import MemberAddResponse as MemberAddResponse from .member_list_response import MemberListResponse as MemberListResponse -from .member_remove_response import MemberRemoveResponse as MemberRemoveResponse diff --git a/src/blooio/types/groups/member_add_response.py b/src/blooio/types/groups/member_add_response.py deleted file mode 100644 index 606e03e..0000000 --- a/src/blooio/types/groups/member_add_response.py +++ /dev/null @@ -1,14 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel -from .group_member import GroupMember - -__all__ = ["MemberAddResponse"] - - -class MemberAddResponse(BaseModel): - member: Optional[GroupMember] = None - - message: Optional[str] = None diff --git a/src/blooio/types/groups/member_remove_response.py b/src/blooio/types/groups/member_remove_response.py deleted file mode 100644 index e1cfe9c..0000000 --- a/src/blooio/types/groups/member_remove_response.py +++ /dev/null @@ -1,13 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional - -from ..._models import BaseModel - -__all__ = ["MemberRemoveResponse"] - - -class MemberRemoveResponse(BaseModel): - removed_at: Optional[int] = None - - success: Optional[bool] = None diff --git a/tests/api_resources/groups/test_members.py b/tests/api_resources/groups/test_members.py index 8ccc6ba..fd7b097 100644 --- a/tests/api_resources/groups/test_members.py +++ b/tests/api_resources/groups/test_members.py @@ -9,11 +9,7 @@ from blooio import Blooio, AsyncBlooio from tests.utils import assert_matches_type -from blooio.types.groups import ( - MemberAddResponse, - MemberListResponse, - MemberRemoveResponse, -) +from blooio.types.groups import MemberListResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -80,7 +76,7 @@ def test_method_add(self, client: Blooio) -> None: group_id="grp_abc123def456", contact_id="+15551234567", ) - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -93,7 +89,7 @@ def test_raw_response_add(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -106,7 +102,7 @@ def test_streaming_response_add(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True @@ -126,7 +122,7 @@ def test_method_remove(self, client: Blooio) -> None: contact_id="%2B15551234567", group_id="grp_abc123def456", ) - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -139,7 +135,7 @@ def test_raw_response_remove(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -152,7 +148,7 @@ def test_streaming_response_remove(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True @@ -236,7 +232,7 @@ async def test_method_add(self, async_client: AsyncBlooio) -> None: group_id="grp_abc123def456", contact_id="+15551234567", ) - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -249,7 +245,7 @@ async def test_raw_response_add(self, async_client: AsyncBlooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -262,7 +258,7 @@ async def test_streaming_response_add(self, async_client: AsyncBlooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberAddResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True @@ -282,7 +278,7 @@ async def test_method_remove(self, async_client: AsyncBlooio) -> None: contact_id="%2B15551234567", group_id="grp_abc123def456", ) - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -295,7 +291,7 @@ async def test_raw_response_remove(self, async_client: AsyncBlooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -308,7 +304,7 @@ async def test_streaming_response_remove(self, async_client: AsyncBlooio) -> Non assert response.http_request.headers.get("X-Stainless-Lang") == "python" member = await response.parse() - assert_matches_type(MemberRemoveResponse, member, path=["response"]) + assert member is None assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_facetime.py b/tests/api_resources/test_facetime.py index 3611d91..9d5d2cd 100644 --- a/tests/api_resources/test_facetime.py +++ b/tests/api_resources/test_facetime.py @@ -8,8 +8,6 @@ import pytest from blooio import Blooio, AsyncBlooio -from tests.utils import assert_matches_type -from blooio.types import FacetimeInitiateCallResponse base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -23,7 +21,7 @@ def test_method_initiate_call(self, client: Blooio) -> None: facetime = client.facetime.initiate_call( handle="+15551234567", ) - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -35,7 +33,7 @@ def test_raw_response_initiate_call(self, client: Blooio) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -47,7 +45,7 @@ def test_streaming_response_initiate_call(self, client: Blooio) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None assert cast(Any, response.is_closed) is True @@ -63,7 +61,7 @@ async def test_method_initiate_call(self, async_client: AsyncBlooio) -> None: facetime = await async_client.facetime.initiate_call( handle="+15551234567", ) - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -75,7 +73,7 @@ async def test_raw_response_initiate_call(self, async_client: AsyncBlooio) -> No assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = await response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -87,6 +85,6 @@ async def test_streaming_response_initiate_call(self, async_client: AsyncBlooio) assert response.http_request.headers.get("X-Stainless-Lang") == "python" facetime = await response.parse() - assert_matches_type(FacetimeInitiateCallResponse, facetime, path=["response"]) + assert facetime is None assert cast(Any, response.is_closed) is True From d519459512a1adfa7a3caf17d534989f1769a0c2 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 19:34:22 +0000 Subject: [PATCH 24/26] feat(api): api update --- .stats.yml | 4 ++-- .../types/webhooks/log_list_response.py | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index 9c9d64a..c4e1315 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-14138ff0ac1c85c9b0e958c0596bd8deec484fdeadb4199d0c6c21d51b513f69.yml -openapi_spec_hash: 4814e4ad505a416e0b44b658ce7b2f5d +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-06edfb685a933de4b626b9526ae935b2959b9a654e010842c979757e836763db.yml +openapi_spec_hash: 8118b6bc33934af85760f04d4b88052b config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/types/webhooks/log_list_response.py b/src/blooio/types/webhooks/log_list_response.py index a85d793..4e2c819 100644 --- a/src/blooio/types/webhooks/log_list_response.py +++ b/src/blooio/types/webhooks/log_list_response.py @@ -39,6 +39,22 @@ class LogEventBody(BaseModel): attachments: Optional[List[LogEventBodyAttachment]] = None """Array of attachment objects""" + chat_guid: Optional[str] = None + """ + The device's own identifier for the group conversation this message arrived in + (only on `message.received` when is_group=true). Two group chats can hold the + same members and are then indistinguishable by `group_id` and `participants` + alone; `chat_guid` is what tells them apart. Matches the `chat_guid` on GET + /groups/{groupId}. + """ + + chat_name: Optional[str] = None + """ + The name the device reports for the conversation (only on `message.received` + when is_group=true). May differ from `group_name`, or be present when + `group_name` is null. + """ + delivered_at: Optional[int] = None """Timestamp when message was delivered (for message.delivered events)""" @@ -73,7 +89,11 @@ class LogEventBody(BaseModel): """Unique message identifier""" participants: Optional[List[LogEventBodyParticipant]] = None - """Array of group participants (only present when is_group=true)""" + """Array of group participants (only present when is_group=true). + + One entry per person: a participant appears once even if Blooio holds more than + one identity for their number. + """ protocol: Optional[Literal["pending", "unknown", "imessage", "sms", "rcs"]] = None """Transport used to carry the message; never null. From b2d50ea9ab22a13e8538ceff4193e5267992cc85 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:20:35 +0000 Subject: [PATCH 25/26] feat(api): api update --- .stats.yml | 4 +- src/blooio/resources/chats/messages.py | 82 +++++++++++++++++++ .../types/chats/message_list_response.py | 20 +++++ .../types/chats/message_retrieve_response.py | 20 +++++ src/blooio/types/chats/message_send_params.py | 42 ++++++++++ .../types/webhooks/log_list_response.py | 20 +++++ tests/api_resources/chats/test_messages.py | 2 + 7 files changed, 188 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index c4e1315..d4e4ced 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 54 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-06edfb685a933de4b626b9526ae935b2959b9a654e010842c979757e836763db.yml -openapi_spec_hash: 8118b6bc33934af85760f04d4b88052b +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/blooio/blooio-18cad1cfa869b6c0be9b0ef7573af1162034b9ae13a100a7378967f8633619f5.yml +openapi_spec_hash: ad3171218a23a9c40695fd30a67758fa config_hash: 839e191496287972d18d39dcab8b19b7 diff --git a/src/blooio/resources/chats/messages.py b/src/blooio/resources/chats/messages.py index ce33663..1db03e7 100644 --- a/src/blooio/resources/chats/messages.py +++ b/src/blooio/resources/chats/messages.py @@ -281,6 +281,7 @@ def send( ] ] | Omit = omit, + format: Literal["plain", "markdown"] | Omit = omit, from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, @@ -367,6 +368,45 @@ def send( - When `text` is an array, every message in the array is sent with the same effect. + format: How to interpret `text` (and each `parts[].text`). Defaults to `plain`, which + sends the string exactly as given. + + With `markdown`, four constructs are parsed and delivered as real iMessage rich + text — the recipient sees styled text, not delimiters: + + | Construct | Syntax | + | ------------- | ------------------------ | + | Bold | `**bold**` or `__bold__` | + | Italic | `*italic*` or `_italic_` | + | Underline | `++underline++` | + | Strikethrough | `~~strike~~` | + + They nest freely (`**bold and _italic_**`). Everything else Markdown can express + — headings, lists, links, code spans, blockquotes, images — is NOT styling + iMessage can carry, so it is passed through as literal characters: + `[Blooio](https://blooio.com)` is delivered with its brackets and URL intact, + and `# Heading` keeps its `#`. Escape a delimiter with a backslash + (`\\**not italic\\**`) to send it literally. + + The styling travels in the message's attributed body, so the stored `text` and + the `text` returned on reads and webhooks is always the plain string the + recipient sees, with the delimiters removed. The Markdown itself comes back as + `formatted_text`, re-serialized into a normalized spelling rather than echoed + verbatim (`__bold__` returns as `**bold**`). + + Only valid on Blooio iMessage channels — + `400 format_unsupported_for_channel_type` on any other channel type, since no + other channel type has a rich-text equivalent and would otherwise deliver your + delimiters as literal text. Rich text also requires the message to be delivered + over iMessage: a Blooio send that falls back to SMS arrives as unstyled plain + text (the `text` string), because SMS cannot carry styling. + + Applies to a text send and to `parts`. Rejected with `400 invalid_content` when + combined with `attachments` — a media caption is not a styled bubble, so send + the media and the styled text as two messages — when set without `text` or + `parts`, when the Markdown source exceeds 20000 characters, or when it compiles + to more than 256 distinct formatting ranges. + from_number: E.164 phone number to send from. For Twilio API keys, this is optional — if omitted, the first assigned Twilio number is auto-selected. For Blooio (iMessage) API keys, this selects a specific number from your pool. Must be a @@ -424,6 +464,7 @@ def send( { "attachments": attachments, "effect": effect, + "format": format, "from_number": from_number, "link_preview": link_preview, "parts": parts, @@ -693,6 +734,7 @@ async def send( ] ] | Omit = omit, + format: Literal["plain", "markdown"] | Omit = omit, from_number: str | Omit = omit, link_preview: Optional[LinkPreviewParam] | Omit = omit, parts: Iterable[message_send_params.Part] | Omit = omit, @@ -779,6 +821,45 @@ async def send( - When `text` is an array, every message in the array is sent with the same effect. + format: How to interpret `text` (and each `parts[].text`). Defaults to `plain`, which + sends the string exactly as given. + + With `markdown`, four constructs are parsed and delivered as real iMessage rich + text — the recipient sees styled text, not delimiters: + + | Construct | Syntax | + | ------------- | ------------------------ | + | Bold | `**bold**` or `__bold__` | + | Italic | `*italic*` or `_italic_` | + | Underline | `++underline++` | + | Strikethrough | `~~strike~~` | + + They nest freely (`**bold and _italic_**`). Everything else Markdown can express + — headings, lists, links, code spans, blockquotes, images — is NOT styling + iMessage can carry, so it is passed through as literal characters: + `[Blooio](https://blooio.com)` is delivered with its brackets and URL intact, + and `# Heading` keeps its `#`. Escape a delimiter with a backslash + (`\\**not italic\\**`) to send it literally. + + The styling travels in the message's attributed body, so the stored `text` and + the `text` returned on reads and webhooks is always the plain string the + recipient sees, with the delimiters removed. The Markdown itself comes back as + `formatted_text`, re-serialized into a normalized spelling rather than echoed + verbatim (`__bold__` returns as `**bold**`). + + Only valid on Blooio iMessage channels — + `400 format_unsupported_for_channel_type` on any other channel type, since no + other channel type has a rich-text equivalent and would otherwise deliver your + delimiters as literal text. Rich text also requires the message to be delivered + over iMessage: a Blooio send that falls back to SMS arrives as unstyled plain + text (the `text` string), because SMS cannot carry styling. + + Applies to a text send and to `parts`. Rejected with `400 invalid_content` when + combined with `attachments` — a media caption is not a styled bubble, so send + the media and the styled text as two messages — when set without `text` or + `parts`, when the Markdown source exceeds 20000 characters, or when it compiles + to more than 256 distinct formatting ranges. + from_number: E.164 phone number to send from. For Twilio API keys, this is optional — if omitted, the first assigned Twilio number is auto-selected. For Blooio (iMessage) API keys, this selects a specific number from your pool. Must be a @@ -836,6 +917,7 @@ async def send( { "attachments": attachments, "effect": effect, + "format": format, "from_number": from_number, "link_preview": link_preview, "parts": parts, diff --git a/src/blooio/types/chats/message_list_response.py b/src/blooio/types/chats/message_list_response.py index c24c43c..0bd58ad 100644 --- a/src/blooio/types/chats/message_list_response.py +++ b/src/blooio/types/chats/message_list_response.py @@ -44,6 +44,26 @@ class Message(BaseModel): external_id: Optional[str] = None """Phone number or email of the contact, or group ID for group messages""" + formatted_text: Optional[str] = None + """Markdown for a rich-text (bold/italic/underline/strikethrough) message. + + Omitted entirely when the message carries no styling, so its presence is how you + detect rich text. + + Present in both directions: on an outbound send made with `format: "markdown"`, + and on an inbound iMessage whose sender styled their text — so styling a + customer applied in Messages arrives here even though your integration never + asked for it. + + Always a normalized re-serialization of the message's actual styling rather than + an echo of the source string: bold is spelled `**`, italic `*`, underline `++`, + strikethrough `~~`, and any character that would otherwise read as a delimiter + is backslash-escaped. Re-sending this value verbatim with `format: "markdown"` + reproduces the same styled message. Blooio iMessage only. This is the SAME field + delivered on the message webhooks, so a message reads identically via REST or + webhook. + """ + internal_id: Optional[str] = None """Organization phone number (from-number) used for this message""" diff --git a/src/blooio/types/chats/message_retrieve_response.py b/src/blooio/types/chats/message_retrieve_response.py index 384d78b..1299033 100644 --- a/src/blooio/types/chats/message_retrieve_response.py +++ b/src/blooio/types/chats/message_retrieve_response.py @@ -53,6 +53,26 @@ class MessageRetrieveResponse(BaseModel): error: Optional[str] = None + formatted_text: Optional[str] = None + """Markdown for a rich-text (bold/italic/underline/strikethrough) message. + + Omitted entirely when the message carries no styling, so its presence is how you + detect rich text. + + Present in both directions: on an outbound send made with `format: "markdown"`, + and on an inbound iMessage whose sender styled their text — so styling a + customer applied in Messages arrives here even though your integration never + asked for it. + + Always a normalized re-serialization of the message's actual styling rather than + an echo of the source string: bold is spelled `**`, italic `*`, underline `++`, + strikethrough `~~`, and any character that would otherwise read as a delimiter + is backslash-escaped. Re-sending this value verbatim with `format: "markdown"` + reproduces the same styled message. Blooio iMessage only. This is the SAME field + delivered on the message webhooks, so a message reads identically via REST or + webhook. + """ + internal_id: Optional[str] = None """Organization phone number (from-number) used for this message""" diff --git a/src/blooio/types/chats/message_send_params.py b/src/blooio/types/chats/message_send_params.py index 8075848..7f7072a 100644 --- a/src/blooio/types/chats/message_send_params.py +++ b/src/blooio/types/chats/message_send_params.py @@ -74,6 +74,48 @@ class MessageSendParams(TypedDict, total=False): effect. """ + format: Literal["plain", "markdown"] + """How to interpret `text` (and each `parts[].text`). + + Defaults to `plain`, which sends the string exactly as given. + + With `markdown`, four constructs are parsed and delivered as real iMessage rich + text — the recipient sees styled text, not delimiters: + + | Construct | Syntax | + | ------------- | ------------------------ | + | Bold | `**bold**` or `__bold__` | + | Italic | `*italic*` or `_italic_` | + | Underline | `++underline++` | + | Strikethrough | `~~strike~~` | + + They nest freely (`**bold and _italic_**`). Everything else Markdown can express + — headings, lists, links, code spans, blockquotes, images — is NOT styling + iMessage can carry, so it is passed through as literal characters: + `[Blooio](https://blooio.com)` is delivered with its brackets and URL intact, + and `# Heading` keeps its `#`. Escape a delimiter with a backslash + (`\\**not italic\\**`) to send it literally. + + The styling travels in the message's attributed body, so the stored `text` and + the `text` returned on reads and webhooks is always the plain string the + recipient sees, with the delimiters removed. The Markdown itself comes back as + `formatted_text`, re-serialized into a normalized spelling rather than echoed + verbatim (`__bold__` returns as `**bold**`). + + Only valid on Blooio iMessage channels — + `400 format_unsupported_for_channel_type` on any other channel type, since no + other channel type has a rich-text equivalent and would otherwise deliver your + delimiters as literal text. Rich text also requires the message to be delivered + over iMessage: a Blooio send that falls back to SMS arrives as unstyled plain + text (the `text` string), because SMS cannot carry styling. + + Applies to a text send and to `parts`. Rejected with `400 invalid_content` when + combined with `attachments` — a media caption is not a styled bubble, so send + the media and the styled text as two messages — when set without `text` or + `parts`, when the Markdown source exceeds 20000 characters, or when it compiles + to more than 256 distinct formatting ranges. + """ + from_number: str """E.164 phone number to send from. diff --git a/src/blooio/types/webhooks/log_list_response.py b/src/blooio/types/webhooks/log_list_response.py index 4e2c819..a5f9a9b 100644 --- a/src/blooio/types/webhooks/log_list_response.py +++ b/src/blooio/types/webhooks/log_list_response.py @@ -73,6 +73,26 @@ class LogEventBody(BaseModel): external_id: Optional[str] = None """Recipient identifier (phone number, email, or group ID)""" + formatted_text: Optional[str] = None + """Markdown for a rich-text (bold/italic/underline/strikethrough) message. + + Omitted entirely when the message carries no styling, so its presence is how you + detect rich text. + + Present in both directions: on an outbound send made with `format: "markdown"`, + and on an inbound iMessage whose sender styled their text — so styling a + customer applied in Messages arrives here even though your integration never + asked for it. + + Always a normalized re-serialization of the message's actual styling rather than + an echo of the source string: bold is spelled `**`, italic `*`, underline `++`, + strikethrough `~~`, and any character that would otherwise read as a delimiter + is backslash-escaped. Re-sending this value verbatim with `format: "markdown"` + reproduces the same styled message. Blooio iMessage only. This is the SAME field + delivered on the message webhooks, so a message reads identically via REST or + webhook. + """ + group_id: Optional[str] = None """Group ID (only present when is_group=true)""" diff --git a/tests/api_resources/chats/test_messages.py b/tests/api_resources/chats/test_messages.py index fddd22b..4af40bf 100644 --- a/tests/api_resources/chats/test_messages.py +++ b/tests/api_resources/chats/test_messages.py @@ -266,6 +266,7 @@ def test_method_send_with_all_params(self, client: Blooio) -> None: chat_id="chatId", attachments=["string"], effect="slam", + format="plain", from_number="from_number", link_preview={ "image_url": "https://example.com", @@ -578,6 +579,7 @@ async def test_method_send_with_all_params(self, async_client: AsyncBlooio) -> N chat_id="chatId", attachments=["string"], effect="slam", + format="plain", from_number="from_number", link_preview={ "image_url": "https://example.com", From 300043662b9904465d57370ce37cf9c646dedd9d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 11:21:13 +0000 Subject: [PATCH 26/26] release: 1.3.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- src/blooio/_version.py | 2 +- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d0ab664..2a8f4ff 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.2.0" + ".": "1.3.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ba8d78c..015d83a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## 1.3.0 (2026-09-03) + +Full Changelog: [v1.2.0...v1.3.0](https://github.com/Blooio/blooio-python-sdk/compare/v1.2.0...v1.3.0) + +### Features + +* **api:** api update ([b2d50ea](https://github.com/Blooio/blooio-python-sdk/commit/b2d50ea9ab22a13e8538ceff4193e5267992cc85)) +* **api:** api update ([d519459](https://github.com/Blooio/blooio-python-sdk/commit/d519459512a1adfa7a3caf17d534989f1769a0c2)) +* **api:** api update ([e423382](https://github.com/Blooio/blooio-python-sdk/commit/e4233820761194e86157825cb2fd1aa3925357da)) +* **api:** api update ([cf471a5](https://github.com/Blooio/blooio-python-sdk/commit/cf471a562375341931981795ed31394d650718c0)) +* **api:** api update ([c8e6827](https://github.com/Blooio/blooio-python-sdk/commit/c8e68277578cd19811353635d943a5589c705a30)) +* **api:** api update ([f725a2a](https://github.com/Blooio/blooio-python-sdk/commit/f725a2a9f09573cd3f72b6d0457a9eb807f2ddd9)) +* **api:** api update ([ef1df40](https://github.com/Blooio/blooio-python-sdk/commit/ef1df408e0298e430c14a26942564e5a77aa259c)) +* **api:** api update ([30e8dd1](https://github.com/Blooio/blooio-python-sdk/commit/30e8dd13b8e2ca5988573a2cc482c122ea1650ae)) +* **api:** api update ([f6caf8e](https://github.com/Blooio/blooio-python-sdk/commit/f6caf8e2ab1630818c57ab8142a5107cc534c57c)) +* **api:** api update ([b3ba795](https://github.com/Blooio/blooio-python-sdk/commit/b3ba7951d022e732ae0e3a458520ed3f42a49a5f)) +* **api:** api update ([d9b82ea](https://github.com/Blooio/blooio-python-sdk/commit/d9b82ea21659239202fb8a4d63366d54ea60fcbe)) +* **api:** api update ([2e075ad](https://github.com/Blooio/blooio-python-sdk/commit/2e075ad61159d021fc31bd6fda9f0152ea703a34)) +* **api:** api update ([178129e](https://github.com/Blooio/blooio-python-sdk/commit/178129ec16b48bb7be95b672071147fb1a4bf679)) +* **api:** api update ([03aecef](https://github.com/Blooio/blooio-python-sdk/commit/03aecefb518d40f36c37d71cfa7637cefb0575c4)) +* **api:** api update ([e4708cb](https://github.com/Blooio/blooio-python-sdk/commit/e4708cb01510e75961ac6a058308140cf69424ee)) +* **api:** api update ([bb8074b](https://github.com/Blooio/blooio-python-sdk/commit/bb8074b817bc550cba91e705ebbd710dee71caba)) +* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([33ac236](https://github.com/Blooio/blooio-python-sdk/commit/33ac236b6e204193681f2db4822517a0de75b182)) + + +### Bug Fixes + +* **auth:** prioritize first auth header ([d692d48](https://github.com/Blooio/blooio-python-sdk/commit/d692d48a74bebaa8b86c6e645941c85ebab88c2e)) +* **internal:** resolve build failures ([12fb122](https://github.com/Blooio/blooio-python-sdk/commit/12fb122bc963ca30ba0d2767352d2f3fde733bfb)) + ## 1.2.0 (2026-05-14) Full Changelog: [v1.1.1...v1.2.0](https://github.com/Blooio/blooio-python-sdk/compare/v1.1.1...v1.2.0) diff --git a/pyproject.toml b/pyproject.toml index 4c6c403..4735d59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "blooio" -version = "1.2.0" +version = "1.3.0" description = "The official Python library for the blooio API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/blooio/_version.py b/src/blooio/_version.py index 2fd6bd0..b50668b 100644 --- a/src/blooio/_version.py +++ b/src/blooio/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "blooio" -__version__ = "1.2.0" # x-release-please-version +__version__ = "1.3.0" # x-release-please-version