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
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/.stats.yml b/.stats.yml
index 000c763..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-bce90b321b6782841d7834e0e0689b1812ef1f10f8f2823cd9908185d9f306e6.yml
-openapi_spec_hash: a90c7586ed5e83a938d72a286efa9f8e
+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/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/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/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/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 .
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]:
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
diff --git a/src/blooio/resources/chats/background.py b/src/blooio/resources/chats/background.py
index a0c1121..e76ec76 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://api.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://api.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..5c77eec 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
@@ -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)
@@ -239,6 +243,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
@@ -279,7 +287,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
@@ -353,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)
@@ -440,6 +452,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
@@ -496,7 +512,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 +552,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 +592,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 +632,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 8f844d3..1db03e7 100644
--- a/src/blooio/resources/chats/messages.py
+++ b/src/blooio/resources/chats/messages.py
@@ -105,12 +105,20 @@ 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
- 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)
@@ -209,8 +217,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 +226,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)
@@ -275,9 +281,11 @@ 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,
+ 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,8 +312,28 @@ 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
+ 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.
@@ -340,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
@@ -363,8 +430,17 @@ 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.
+ 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)
@@ -388,9 +464,11 @@ def send(
{
"attachments": attachments,
"effect": effect,
+ "format": format,
"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,
@@ -480,12 +558,20 @@ 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
- 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)
@@ -584,8 +670,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 +679,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)
@@ -650,9 +734,11 @@ 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,
+ 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,8 +765,28 @@ 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
+ 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.
@@ -715,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
@@ -738,8 +883,17 @@ 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.
+ 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)
@@ -763,9 +917,11 @@ async def send(
{
"attachments": attachments,
"effect": effect,
+ "format": format,
"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/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
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/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/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..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"]
@@ -62,9 +60,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
@@ -105,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.
@@ -126,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(
@@ -146,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.
@@ -168,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,
)
@@ -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
@@ -259,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.
@@ -280,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(
@@ -300,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.
@@ -322,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/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/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/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/__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/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/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/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 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_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_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/chats/message_list_response.py b/src/blooio/types/chats/message_list_response.py
index 88001a7..0bd58ad 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):
@@ -20,16 +44,51 @@ 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"""
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)"""
+ 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.
@@ -39,6 +98,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_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"]
diff --git a/src/blooio/types/chats/message_retrieve_response.py b/src/blooio/types/chats/message_retrieve_response.py
index a2c61ad..1299033 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
@@ -29,16 +53,51 @@ 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"""
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)"""
+ 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.
@@ -48,6 +107,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_params.py b/src/blooio/types/chats/message_send_params.py
index b2b935b..7f7072a 100644
--- a/src/blooio/types/chats/message_send_params.py
+++ b/src/blooio/types/chats/message_send_params.py
@@ -9,12 +9,19 @@
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):
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[
@@ -67,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.
@@ -97,10 +146,22 @@ 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.
- 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]]
@@ -149,3 +210,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..13efb90 100644
--- a/src/blooio/types/chats/message_send_response.py
+++ b/src/blooio/types/chats/message_send_response.py
@@ -30,8 +30,21 @@ 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)"""
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/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/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/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/__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_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/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/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/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"""
diff --git a/src/blooio/types/webhooks/log_list_response.py b/src/blooio/types/webhooks/log_list_response.py
index 386c9d6..a5f9a9b 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)"""
@@ -57,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)"""
@@ -73,10 +109,21 @@ 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["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 +135,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"""
diff --git a/tests/api_resources/chats/test_messages.py b/tests/api_resources/chats/test_messages.py
index 1ca5b92..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",
@@ -283,6 +284,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,
@@ -573,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",
@@ -590,6 +597,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,
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
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