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