From a2f82f1b07e33f2bb4495ac93406808d00a8b3a4 Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Wed, 5 Aug 2026 22:10:43 +0000 Subject: [PATCH 1/8] docs: clarify presigned url behavior and configuration --- src/linq/types/attachment_create_response.py | 4 +++- uv.lock | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/linq/types/attachment_create_response.py b/src/linq/types/attachment_create_response.py index dbe180b..c6dd48e 100644 --- a/src/linq/types/attachment_create_response.py +++ b/src/linq/types/attachment_create_response.py @@ -37,5 +37,7 @@ class AttachmentCreateResponse(BaseModel): """Presigned URL for uploading the file. PUT the raw binary file content to this URL with the `required_headers`. Do not - JSON-encode or multipart-wrap the body. Expires after 15 minutes. + JSON-encode or multipart-wrap the body. Expires after 15 minutes. Treat the URL + as opaque — the hostname depends on partner configuration and is the same across + sandbox and production. """ diff --git a/uv.lock b/uv.lock index bdb9df0..266f215 100644 --- a/uv.lock +++ b/uv.lock @@ -530,7 +530,7 @@ wheels = [ [[package]] name = "linq-python" -version = "0.19.1" +version = "0.20.0" source = { editable = "." } dependencies = [ { name = "anyio" }, From fa522c848f176ac2f33dda752f1270431105d3c0 Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Sat, 8 Aug 2026 01:08:20 +0000 Subject: [PATCH 2/8] fix: clarify stop keyword behavior and matching rules --- src/linq/types/chat.py | 7 ++++--- src/linq/types/chat_create_response.py | 7 ++++--- src/linq/types/chat_created_webhook_event.py | 7 ++++--- src/linq/types/message_edited_webhook_event.py | 7 ++++--- src/linq/types/message_event_v2.py | 7 ++++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/linq/types/chat.py b/src/linq/types/chat.py index f8f5d1f..8e72b16 100644 --- a/src/linq/types/chat.py +++ b/src/linq/types/chat.py @@ -35,9 +35,10 @@ class HealthStatus(BaseModel): part of a longer one: `STOP` counts, `please stop` does not. Most keywords must match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, - or if they keep replying on the chat — sustained two-way conversation is treated - as a sign the stop keyword was a false positive. + `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — + these match in any casing — or if they keep replying on the chat, since + sustained two-way conversation is treated as a sign the stop keyword was a false + positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/chat_create_response.py b/src/linq/types/chat_create_response.py index e1c34ed..0618f0a 100644 --- a/src/linq/types/chat_create_response.py +++ b/src/linq/types/chat_create_response.py @@ -36,9 +36,10 @@ class ChatHealthStatus(BaseModel): part of a longer one: `STOP` counts, `please stop` does not. Most keywords must match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, - or if they keep replying on the chat — sustained two-way conversation is treated - as a sign the stop keyword was a false positive. + `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — + these match in any casing — or if they keep replying on the chat, since + sustained two-way conversation is treated as a sign the stop keyword was a false + positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/chat_created_webhook_event.py b/src/linq/types/chat_created_webhook_event.py index 0522edf..5658bad 100644 --- a/src/linq/types/chat_created_webhook_event.py +++ b/src/linq/types/chat_created_webhook_event.py @@ -36,9 +36,10 @@ class DataHealthStatus(BaseModel): part of a longer one: `STOP` counts, `please stop` does not. Most keywords must match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, - or if they keep replying on the chat — sustained two-way conversation is treated - as a sign the stop keyword was a false positive. + `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — + these match in any casing — or if they keep replying on the chat, since + sustained two-way conversation is treated as a sign the stop keyword was a false + positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/message_edited_webhook_event.py b/src/linq/types/message_edited_webhook_event.py index 4b52d49..bea3f1c 100644 --- a/src/linq/types/message_edited_webhook_event.py +++ b/src/linq/types/message_edited_webhook_event.py @@ -34,9 +34,10 @@ class DataChatHealthStatus(BaseModel): part of a longer one: `STOP` counts, `please stop` does not. Most keywords must match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, - or if they keep replying on the chat — sustained two-way conversation is treated - as a sign the stop keyword was a false positive. + `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — + these match in any casing — or if they keep replying on the chat, since + sustained two-way conversation is treated as a sign the stop keyword was a false + positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/message_event_v2.py b/src/linq/types/message_event_v2.py index fa20f95..25c33f0 100644 --- a/src/linq/types/message_event_v2.py +++ b/src/linq/types/message_event_v2.py @@ -49,9 +49,10 @@ class ChatHealthStatus(BaseModel): part of a longer one: `STOP` counts, `please stop` does not. Most keywords must match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, - or if they keep replying on the chat — sustained two-way conversation is treated - as a sign the stop keyword was a false positive. + `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — + these match in any casing — or if they keep replying on the chat, since + sustained two-way conversation is treated as a sign the stop keyword was a false + positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across From 1833ad1738c0fdd3133f9b9007c4bf547b3661a5 Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Sun, 9 Aug 2026 20:11:20 +0000 Subject: [PATCH 3/8] fix: clarify stop keyword behavior for multi-chat replies --- src/linq/types/chat.py | 6 +++--- src/linq/types/chat_create_response.py | 6 +++--- src/linq/types/chat_created_webhook_event.py | 6 +++--- src/linq/types/message_edited_webhook_event.py | 6 +++--- src/linq/types/message_event_v2.py | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/linq/types/chat.py b/src/linq/types/chat.py index 8e72b16..f1fd16a 100644 --- a/src/linq/types/chat.py +++ b/src/linq/types/chat.py @@ -36,9 +36,9 @@ class HealthStatus(BaseModel): match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying on the chat, since - sustained two-way conversation is treated as a sign the stop keyword was a false - positive. + these match in any casing — or if they keep replying — replies in any + conversation with you count, the same way the block does — since sustained + two-way conversation is treated as a sign the stop keyword was a false positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/chat_create_response.py b/src/linq/types/chat_create_response.py index 0618f0a..6e8b843 100644 --- a/src/linq/types/chat_create_response.py +++ b/src/linq/types/chat_create_response.py @@ -37,9 +37,9 @@ class ChatHealthStatus(BaseModel): match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying on the chat, since - sustained two-way conversation is treated as a sign the stop keyword was a false - positive. + these match in any casing — or if they keep replying — replies in any + conversation with you count, the same way the block does — since sustained + two-way conversation is treated as a sign the stop keyword was a false positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/chat_created_webhook_event.py b/src/linq/types/chat_created_webhook_event.py index 5658bad..be28803 100644 --- a/src/linq/types/chat_created_webhook_event.py +++ b/src/linq/types/chat_created_webhook_event.py @@ -37,9 +37,9 @@ class DataHealthStatus(BaseModel): match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying on the chat, since - sustained two-way conversation is treated as a sign the stop keyword was a false - positive. + these match in any casing — or if they keep replying — replies in any + conversation with you count, the same way the block does — since sustained + two-way conversation is treated as a sign the stop keyword was a false positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/message_edited_webhook_event.py b/src/linq/types/message_edited_webhook_event.py index bea3f1c..0cf11ce 100644 --- a/src/linq/types/message_edited_webhook_event.py +++ b/src/linq/types/message_edited_webhook_event.py @@ -35,9 +35,9 @@ class DataChatHealthStatus(BaseModel): match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying on the chat, since - sustained two-way conversation is treated as a sign the stop keyword was a false - positive. + these match in any casing — or if they keep replying — replies in any + conversation with you count, the same way the block does — since sustained + two-way conversation is treated as a sign the stop keyword was a false positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/message_event_v2.py b/src/linq/types/message_event_v2.py index 25c33f0..44acae9 100644 --- a/src/linq/types/message_event_v2.py +++ b/src/linq/types/message_event_v2.py @@ -50,9 +50,9 @@ class ChatHealthStatus(BaseModel): match exactly, including case. `OPT OUT` is the exception — it matches in any casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying on the chat, since - sustained two-way conversation is treated as a sign the stop keyword was a false - positive. + these match in any casing — or if they keep replying — replies in any + conversation with you count, the same way the block does — since sustained + two-way conversation is treated as a sign the stop keyword was a false positive. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across From 3564577b56284c6247b85d948b2e221272507185 Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Mon, 10 Aug 2026 14:04:16 +0000 Subject: [PATCH 4/8] fix: expand webhook error code documentation for failure events --- .../types/chat_group_icon_update_failed_webhook_event.py | 8 +++++++- .../types/chat_group_name_update_failed_webhook_event.py | 8 +++++++- src/linq/types/message_failed_webhook_event.py | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/linq/types/chat_group_icon_update_failed_webhook_event.py b/src/linq/types/chat_group_icon_update_failed_webhook_event.py index b15a421..e159d6b 100644 --- a/src/linq/types/chat_group_icon_update_failed_webhook_event.py +++ b/src/linq/types/chat_group_icon_update_failed_webhook_event.py @@ -18,7 +18,13 @@ class Data(BaseModel): """Chat identifier (UUID) of the group chat""" error_code: int - """Error codes in webhook failure events (3007, 4001, 4005).""" + """Error codes in webhook failure events. + + The possible set varies by event: message.failed can carry 3007, 4001, 4002, + 4005, 4006, 4007, or 4008; the group update failure events + (chat.group_name_update_failed, chat.group_icon_update_failed) carry 3007 + or 4001. + """ failed_at: datetime """When the failure was detected""" diff --git a/src/linq/types/chat_group_name_update_failed_webhook_event.py b/src/linq/types/chat_group_name_update_failed_webhook_event.py index bfedbf6..cbe5927 100644 --- a/src/linq/types/chat_group_name_update_failed_webhook_event.py +++ b/src/linq/types/chat_group_name_update_failed_webhook_event.py @@ -18,7 +18,13 @@ class Data(BaseModel): """Chat identifier (UUID) of the group chat""" error_code: int - """Error codes in webhook failure events (3007, 4001, 4005).""" + """Error codes in webhook failure events. + + The possible set varies by event: message.failed can carry 3007, 4001, 4002, + 4005, 4006, 4007, or 4008; the group update failure events + (chat.group_name_update_failed, chat.group_icon_update_failed) carry 3007 + or 4001. + """ failed_at: datetime """When the failure was detected""" diff --git a/src/linq/types/message_failed_webhook_event.py b/src/linq/types/message_failed_webhook_event.py index 1989409..9055281 100644 --- a/src/linq/types/message_failed_webhook_event.py +++ b/src/linq/types/message_failed_webhook_event.py @@ -18,7 +18,13 @@ class Data(BaseModel): """ code: int - """Error codes in webhook failure events (3007, 4001, 4005).""" + """Error codes in webhook failure events. + + The possible set varies by event: message.failed can carry 3007, 4001, 4002, + 4005, 4006, 4007, or 4008; the group update failure events + (chat.group_name_update_failed, chat.group_icon_update_failed) carry 3007 + or 4001. + """ failed_at: datetime """When the failure was detected""" From e536310e8f856201745c71aba4668e059c15533e Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Mon, 10 Aug 2026 14:10:46 +0000 Subject: [PATCH 5/8] feat: add diagnostic fields to message failure webhook events --- src/linq/types/message_failed_webhook_event.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/linq/types/message_failed_webhook_event.py b/src/linq/types/message_failed_webhook_event.py index 9055281..d576b79 100644 --- a/src/linq/types/message_failed_webhook_event.py +++ b/src/linq/types/message_failed_webhook_event.py @@ -2,9 +2,11 @@ from typing import Optional from datetime import datetime +from typing_extensions import Literal from .._models import BaseModel from .webhook_event_type import WebhookEventType +from .shared.service_type import ServiceType __all__ = ["MessageFailedWebhookEvent", "Data"] @@ -32,12 +34,28 @@ class Data(BaseModel): chat_id: Optional[str] = None """Chat identifier (UUID)""" + detail_code: Optional[int] = None + """ + Opaque diagnostic code identifying the specific failure class within `code`. + Values are not enumerated and may change without notice — log it and include it + in support requests, but do not branch on it. + """ + message_id: Optional[str] = None """Message identifier (UUID)""" + preferred_service: Optional[Literal["iMessage", "SMS", "RCS", "auto"]] = None + """Preferred messaging service type. + + Includes "auto" for default fallback behavior. + """ + reason: Optional[str] = None """Human-readable description of the failure""" + service: Optional[ServiceType] = None + """Messaging service type""" + class MessageFailedWebhookEvent(BaseModel): """Complete webhook payload for message.failed events""" From 0fc4da7cb84e15511c7d58dfeb8647c53e6df3e2 Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Mon, 10 Aug 2026 19:15:08 +0000 Subject: [PATCH 6/8] fix: clarify opted-out status clearing behavior in chat health --- src/linq/types/chat.py | 17 ++++++++--------- src/linq/types/chat_create_response.py | 17 ++++++++--------- src/linq/types/chat_created_webhook_event.py | 17 ++++++++--------- src/linq/types/message_edited_webhook_event.py | 17 ++++++++--------- src/linq/types/message_event_v2.py | 17 ++++++++--------- 5 files changed, 40 insertions(+), 45 deletions(-) diff --git a/src/linq/types/chat.py b/src/linq/types/chat.py index f1fd16a..01a8246 100644 --- a/src/linq/types/chat.py +++ b/src/linq/types/chat.py @@ -30,15 +30,14 @@ class HealthStatus(BaseModel): See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never - part of a longer one: `STOP` counts, `please stop` does not. Most keywords must - match exactly, including case. `OPT OUT` is the exception — it matches in any - casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying — replies in any - conversation with you count, the same way the block does — since sustained - two-way conversation is treated as a sign the stop keyword was a false positive. + `OPTED_OUT` — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, `CANCEL`, + `END`, or `QUIT`. The keyword must be the whole trimmed message, never part of a + longer one: `STOP` counts, `please stop` does not. Most keywords must match + exactly, including case. `OPT OUT` is the exception — it matches in any casing, + with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all + count. It clears as soon as they reply again: any later message from them that + is not itself an opt-out keyword opts them back in immediately — a reply in any + conversation with you counts, the same way the block does. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/chat_create_response.py b/src/linq/types/chat_create_response.py index 6e8b843..fd09d4e 100644 --- a/src/linq/types/chat_create_response.py +++ b/src/linq/types/chat_create_response.py @@ -31,15 +31,14 @@ class ChatHealthStatus(BaseModel): See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never - part of a longer one: `STOP` counts, `please stop` does not. Most keywords must - match exactly, including case. `OPT OUT` is the exception — it matches in any - casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying — replies in any - conversation with you count, the same way the block does — since sustained - two-way conversation is treated as a sign the stop keyword was a false positive. + `OPTED_OUT` — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, `CANCEL`, + `END`, or `QUIT`. The keyword must be the whole trimmed message, never part of a + longer one: `STOP` counts, `please stop` does not. Most keywords must match + exactly, including case. `OPT OUT` is the exception — it matches in any casing, + with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all + count. It clears as soon as they reply again: any later message from them that + is not itself an opt-out keyword opts them back in immediately — a reply in any + conversation with you counts, the same way the block does. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/chat_created_webhook_event.py b/src/linq/types/chat_created_webhook_event.py index be28803..4f7c726 100644 --- a/src/linq/types/chat_created_webhook_event.py +++ b/src/linq/types/chat_created_webhook_event.py @@ -31,15 +31,14 @@ class DataHealthStatus(BaseModel): See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never - part of a longer one: `STOP` counts, `please stop` does not. Most keywords must - match exactly, including case. `OPT OUT` is the exception — it matches in any - casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying — replies in any - conversation with you count, the same way the block does — since sustained - two-way conversation is treated as a sign the stop keyword was a false positive. + `OPTED_OUT` — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, `CANCEL`, + `END`, or `QUIT`. The keyword must be the whole trimmed message, never part of a + longer one: `STOP` counts, `please stop` does not. Most keywords must match + exactly, including case. `OPT OUT` is the exception — it matches in any casing, + with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all + count. It clears as soon as they reply again: any later message from them that + is not itself an opt-out keyword opts them back in immediately — a reply in any + conversation with you counts, the same way the block does. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/message_edited_webhook_event.py b/src/linq/types/message_edited_webhook_event.py index 0cf11ce..60a08a0 100644 --- a/src/linq/types/message_edited_webhook_event.py +++ b/src/linq/types/message_edited_webhook_event.py @@ -29,15 +29,14 @@ class DataChatHealthStatus(BaseModel): See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never - part of a longer one: `STOP` counts, `please stop` does not. Most keywords must - match exactly, including case. `OPT OUT` is the exception — it matches in any - casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying — replies in any - conversation with you count, the same way the block does — since sustained - two-way conversation is treated as a sign the stop keyword was a false positive. + `OPTED_OUT` — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, `CANCEL`, + `END`, or `QUIT`. The keyword must be the whole trimmed message, never part of a + longer one: `STOP` counts, `please stop` does not. Most keywords must match + exactly, including case. `OPT OUT` is the exception — it matches in any casing, + with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all + count. It clears as soon as they reply again: any later message from them that + is not itself an opt-out keyword opts them back in immediately — a reply in any + conversation with you counts, the same way the block does. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across diff --git a/src/linq/types/message_event_v2.py b/src/linq/types/message_event_v2.py index 44acae9..8be2236 100644 --- a/src/linq/types/message_event_v2.py +++ b/src/linq/types/message_event_v2.py @@ -44,15 +44,14 @@ class ChatHealthStatus(BaseModel): See the [Chat Health guide](/guides/chats/chat-health) for what each value means and how to react. `doc_url` deep-links to the relevant section. - `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never - part of a longer one: `STOP` counts, `please stop` does not. Most keywords must - match exactly, including case. `OPT OUT` is the exception — it matches in any - casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and - `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP` — - these match in any casing — or if they keep replying — replies in any - conversation with you count, the same way the block does — since sustained - two-way conversation is treated as a sign the stop keyword was a false positive. + `OPTED_OUT` — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, `CANCEL`, + `END`, or `QUIT`. The keyword must be the whole trimmed message, never part of a + longer one: `STOP` counts, `please stop` does not. Most keywords must match + exactly, including case. `OPT OUT` is the exception — it matches in any casing, + with or without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all + count. It clears as soon as they reply again: any later message from them that + is not itself an opt-out keyword opts them back in immediately — a reply in any + conversation with you counts, the same way the block does. Linq enforces this: while a recipient is opted out, every send to them is rejected with `403` (error code `2024`) before the message is queued, across From 9441af928fdf83af05877c97480d47d8a0b3cdef Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Mon, 10 Aug 2026 19:53:04 +0000 Subject: [PATCH 7/8] feat: add blocked handles api for managing inbound message filtering --- .stats.yml | 2 +- api.md | 14 + src/linq/_client.py | 86 ++++ src/linq/resources/__init__.py | 14 + src/linq/resources/blocked_handles.py | 368 ++++++++++++++++++ src/linq/types/__init__.py | 5 + src/linq/types/blocked_handle_block_params.py | 18 + .../types/blocked_handle_block_response.py | 10 + src/linq/types/blocked_handle_entry.py | 22 ++ .../types/blocked_handle_list_response.py | 13 + .../types/blocked_handle_unblock_params.py | 12 + tests/api_resources/test_blocked_handles.py | 237 +++++++++++ 12 files changed, 800 insertions(+), 1 deletion(-) create mode 100644 src/linq/resources/blocked_handles.py create mode 100644 src/linq/types/blocked_handle_block_params.py create mode 100644 src/linq/types/blocked_handle_block_response.py create mode 100644 src/linq/types/blocked_handle_entry.py create mode 100644 src/linq/types/blocked_handle_list_response.py create mode 100644 src/linq/types/blocked_handle_unblock_params.py create mode 100644 tests/api_resources/test_blocked_handles.py diff --git a/.stats.yml b/.stats.yml index 1c9b72d..d24d405 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1 @@ -configured_endpoints: 61 +configured_endpoints: 64 diff --git a/api.md b/api.md index b0a85d4..e2e7808 100644 --- a/api.md +++ b/api.md @@ -242,6 +242,20 @@ Methods: - client.payments.cancel(payment_id) -> Payment - client.payments.credentials(payment_id) -> PaymentCredentialsResponse +# BlockedHandles + +Types: + +```python +from linq.types import BlockedHandleEntry, BlockedHandleListResponse, BlockedHandleBlockResponse +``` + +Methods: + +- client.blocked_handles.list() -> BlockedHandleListResponse +- client.blocked_handles.block(\*\*params) -> BlockedHandleBlockResponse +- client.blocked_handles.unblock(\*\*params) -> None + # Experiences Types: diff --git a/src/linq/_client.py b/src/linq/_client.py index 10d9dde..769366f 100644 --- a/src/linq/_client.py +++ b/src/linq/_client.py @@ -48,6 +48,7 @@ phonenumbers, phone_numbers, webhook_events, + blocked_handles, payment_handles, available_number, payment_requests, @@ -64,6 +65,7 @@ from .resources.phonenumbers import PhonenumbersResource, AsyncPhonenumbersResource from .resources.phone_numbers import PhoneNumbersResource, AsyncPhoneNumbersResource from .resources.webhook_events import WebhookEventsResource, AsyncWebhookEventsResource + from .resources.blocked_handles import BlockedHandlesResource, AsyncBlockedHandlesResource from .resources.payment_handles import PaymentHandlesResource, AsyncPaymentHandlesResource from .resources.available_number import AvailableNumberResource, AsyncAvailableNumberResource from .resources.payment_requests import PaymentRequestsResource, AsyncPaymentRequestsResource @@ -607,6 +609,20 @@ def payments(self) -> PaymentsResource: return PaymentsResource(self) + @cached_property + def blocked_handles(self) -> BlockedHandlesResource: + """Block handles — phone numbers, email addresses, SMS short codes, or + sender IDs. + + Inbound messages from a blocked handle are dropped before + they reach your webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include + unblocked members are not restricted. + """ + from .resources.blocked_handles import BlockedHandlesResource + + return BlockedHandlesResource(self) + @cached_property def experiences(self) -> ExperiencesResource: """ @@ -1561,6 +1577,20 @@ def payments(self) -> AsyncPaymentsResource: return AsyncPaymentsResource(self) + @cached_property + def blocked_handles(self) -> AsyncBlockedHandlesResource: + """Block handles — phone numbers, email addresses, SMS short codes, or + sender IDs. + + Inbound messages from a blocked handle are dropped before + they reach your webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include + unblocked members are not restricted. + """ + from .resources.blocked_handles import AsyncBlockedHandlesResource + + return AsyncBlockedHandlesResource(self) + @cached_property def experiences(self) -> AsyncExperiencesResource: """ @@ -2449,6 +2479,20 @@ def payments(self) -> payments.PaymentsResourceWithRawResponse: return PaymentsResourceWithRawResponse(self._client.payments) + @cached_property + def blocked_handles(self) -> blocked_handles.BlockedHandlesResourceWithRawResponse: + """Block handles — phone numbers, email addresses, SMS short codes, or + sender IDs. + + Inbound messages from a blocked handle are dropped before + they reach your webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include + unblocked members are not restricted. + """ + from .resources.blocked_handles import BlockedHandlesResourceWithRawResponse + + return BlockedHandlesResourceWithRawResponse(self._client.blocked_handles) + @cached_property def experiences(self) -> experiences.ExperiencesResourceWithRawResponse: """ @@ -3209,6 +3253,20 @@ def payments(self) -> payments.AsyncPaymentsResourceWithRawResponse: return AsyncPaymentsResourceWithRawResponse(self._client.payments) + @cached_property + def blocked_handles(self) -> blocked_handles.AsyncBlockedHandlesResourceWithRawResponse: + """Block handles — phone numbers, email addresses, SMS short codes, or + sender IDs. + + Inbound messages from a blocked handle are dropped before + they reach your webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include + unblocked members are not restricted. + """ + from .resources.blocked_handles import AsyncBlockedHandlesResourceWithRawResponse + + return AsyncBlockedHandlesResourceWithRawResponse(self._client.blocked_handles) + @cached_property def experiences(self) -> experiences.AsyncExperiencesResourceWithRawResponse: """ @@ -3969,6 +4027,20 @@ def payments(self) -> payments.PaymentsResourceWithStreamingResponse: return PaymentsResourceWithStreamingResponse(self._client.payments) + @cached_property + def blocked_handles(self) -> blocked_handles.BlockedHandlesResourceWithStreamingResponse: + """Block handles — phone numbers, email addresses, SMS short codes, or + sender IDs. + + Inbound messages from a blocked handle are dropped before + they reach your webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include + unblocked members are not restricted. + """ + from .resources.blocked_handles import BlockedHandlesResourceWithStreamingResponse + + return BlockedHandlesResourceWithStreamingResponse(self._client.blocked_handles) + @cached_property def experiences(self) -> experiences.ExperiencesResourceWithStreamingResponse: """ @@ -4729,6 +4801,20 @@ def payments(self) -> payments.AsyncPaymentsResourceWithStreamingResponse: return AsyncPaymentsResourceWithStreamingResponse(self._client.payments) + @cached_property + def blocked_handles(self) -> blocked_handles.AsyncBlockedHandlesResourceWithStreamingResponse: + """Block handles — phone numbers, email addresses, SMS short codes, or + sender IDs. + + Inbound messages from a blocked handle are dropped before + they reach your webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include + unblocked members are not restricted. + """ + from .resources.blocked_handles import AsyncBlockedHandlesResourceWithStreamingResponse + + return AsyncBlockedHandlesResourceWithStreamingResponse(self._client.blocked_handles) + @cached_property def experiences(self) -> experiences.AsyncExperiencesResourceWithStreamingResponse: """ diff --git a/src/linq/resources/__init__.py b/src/linq/resources/__init__.py index 018caad..ae80049 100644 --- a/src/linq/resources/__init__.py +++ b/src/linq/resources/__init__.py @@ -81,6 +81,14 @@ WebhookEventsResourceWithStreamingResponse, AsyncWebhookEventsResourceWithStreamingResponse, ) +from .blocked_handles import ( + BlockedHandlesResource, + AsyncBlockedHandlesResource, + BlockedHandlesResourceWithRawResponse, + AsyncBlockedHandlesResourceWithRawResponse, + BlockedHandlesResourceWithStreamingResponse, + AsyncBlockedHandlesResourceWithStreamingResponse, +) from .payment_handles import ( PaymentHandlesResource, AsyncPaymentHandlesResource, @@ -183,6 +191,12 @@ "AsyncPaymentsResourceWithRawResponse", "PaymentsResourceWithStreamingResponse", "AsyncPaymentsResourceWithStreamingResponse", + "BlockedHandlesResource", + "AsyncBlockedHandlesResource", + "BlockedHandlesResourceWithRawResponse", + "AsyncBlockedHandlesResourceWithRawResponse", + "BlockedHandlesResourceWithStreamingResponse", + "AsyncBlockedHandlesResourceWithStreamingResponse", "ExperiencesResource", "AsyncExperiencesResource", "ExperiencesResourceWithRawResponse", diff --git a/src/linq/resources/blocked_handles.py b/src/linq/resources/blocked_handles.py new file mode 100644 index 0000000..d8e74be --- /dev/null +++ b/src/linq/resources/blocked_handles.py @@ -0,0 +1,368 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import blocked_handle_block_params, blocked_handle_unblock_params +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.blocked_handle_list_response import BlockedHandleListResponse +from ..types.blocked_handle_block_response import BlockedHandleBlockResponse + +__all__ = ["BlockedHandlesResource", "AsyncBlockedHandlesResource"] + + +class BlockedHandlesResource(SyncAPIResource): + """Block handles — phone numbers, email addresses, SMS short codes, or + sender IDs. + + Inbound messages from a blocked handle are dropped before + they reach your webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include + unblocked members are not restricted. + """ + + @cached_property + def with_raw_response(self) -> BlockedHandlesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/linq-team/linq-python#accessing-raw-response-data-eg-headers + """ + return BlockedHandlesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BlockedHandlesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/linq-team/linq-python#with_streaming_response + """ + return BlockedHandlesResourceWithStreamingResponse(self) + + def list( + self, + *, + # 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BlockedHandleListResponse: + """Returns all handles you have blocked. + + Inbound messages from a blocked handle are + dropped and produce no webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include unblocked + members are not restricted. + """ + return self._get( + "/v3/blocked_handles", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BlockedHandleListResponse, + ) + + def block( + self, + *, + handle: str, + reason: str | 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BlockedHandleBlockResponse: + """ + Blocks a handle — an E.164 phone number, an email address (iMessage sender), an + SMS short code (e.g. `262966`), or an alphanumeric sender ID. Inbound messages + from it are dropped and produce no webhooks, and direct sends to it are rejected + with `403` (error code `2026`); group sends that include unblocked members are + not restricted. Blocking is idempotent — re-blocking an already blocked handle + returns the existing entry. + + Args: + handle: The handle to block: an E.164 phone number, an email address, an SMS short code + (3-8 digits), or an alphanumeric sender ID. + + reason: Optional free-text note on why the handle was blocked + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v3/blocked_handles", + body=maybe_transform( + { + "handle": handle, + "reason": reason, + }, + blocked_handle_block_params.BlockedHandleBlockParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BlockedHandleBlockResponse, + ) + + def unblock( + self, + *, + handle: str, + # 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Removes a handle from your blocklist. + + Inbound messages from it will be delivered + again and sends to it are allowed again. The handle goes in the request body, + mirroring block — no URL encoding needed. + + Args: + handle: The handle to unblock + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._delete( + "/v3/blocked_handles", + body=maybe_transform({"handle": handle}, blocked_handle_unblock_params.BlockedHandleUnblockParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncBlockedHandlesResource(AsyncAPIResource): + """Block handles — phone numbers, email addresses, SMS short codes, or + sender IDs. + + Inbound messages from a blocked handle are dropped before + they reach your webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include + unblocked members are not restricted. + """ + + @cached_property + def with_raw_response(self) -> AsyncBlockedHandlesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/linq-team/linq-python#accessing-raw-response-data-eg-headers + """ + return AsyncBlockedHandlesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBlockedHandlesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/linq-team/linq-python#with_streaming_response + """ + return AsyncBlockedHandlesResourceWithStreamingResponse(self) + + async def list( + self, + *, + # 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BlockedHandleListResponse: + """Returns all handles you have blocked. + + Inbound messages from a blocked handle are + dropped and produce no webhooks, and direct sends to a blocked handle are + rejected with `403` (error code `2026`). Group sends that include unblocked + members are not restricted. + """ + return await self._get( + "/v3/blocked_handles", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BlockedHandleListResponse, + ) + + async def block( + self, + *, + handle: str, + reason: str | 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> BlockedHandleBlockResponse: + """ + Blocks a handle — an E.164 phone number, an email address (iMessage sender), an + SMS short code (e.g. `262966`), or an alphanumeric sender ID. Inbound messages + from it are dropped and produce no webhooks, and direct sends to it are rejected + with `403` (error code `2026`); group sends that include unblocked members are + not restricted. Blocking is idempotent — re-blocking an already blocked handle + returns the existing entry. + + Args: + handle: The handle to block: an E.164 phone number, an email address, an SMS short code + (3-8 digits), or an alphanumeric sender ID. + + reason: Optional free-text note on why the handle was blocked + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v3/blocked_handles", + body=await async_maybe_transform( + { + "handle": handle, + "reason": reason, + }, + blocked_handle_block_params.BlockedHandleBlockParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=BlockedHandleBlockResponse, + ) + + async def unblock( + self, + *, + handle: str, + # 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, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Removes a handle from your blocklist. + + Inbound messages from it will be delivered + again and sends to it are allowed again. The handle goes in the request body, + mirroring block — no URL encoding needed. + + Args: + handle: The handle to unblock + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._delete( + "/v3/blocked_handles", + body=await async_maybe_transform( + {"handle": handle}, blocked_handle_unblock_params.BlockedHandleUnblockParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class BlockedHandlesResourceWithRawResponse: + def __init__(self, blocked_handles: BlockedHandlesResource) -> None: + self._blocked_handles = blocked_handles + + self.list = to_raw_response_wrapper( + blocked_handles.list, + ) + self.block = to_raw_response_wrapper( + blocked_handles.block, + ) + self.unblock = to_raw_response_wrapper( + blocked_handles.unblock, + ) + + +class AsyncBlockedHandlesResourceWithRawResponse: + def __init__(self, blocked_handles: AsyncBlockedHandlesResource) -> None: + self._blocked_handles = blocked_handles + + self.list = async_to_raw_response_wrapper( + blocked_handles.list, + ) + self.block = async_to_raw_response_wrapper( + blocked_handles.block, + ) + self.unblock = async_to_raw_response_wrapper( + blocked_handles.unblock, + ) + + +class BlockedHandlesResourceWithStreamingResponse: + def __init__(self, blocked_handles: BlockedHandlesResource) -> None: + self._blocked_handles = blocked_handles + + self.list = to_streamed_response_wrapper( + blocked_handles.list, + ) + self.block = to_streamed_response_wrapper( + blocked_handles.block, + ) + self.unblock = to_streamed_response_wrapper( + blocked_handles.unblock, + ) + + +class AsyncBlockedHandlesResourceWithStreamingResponse: + def __init__(self, blocked_handles: AsyncBlockedHandlesResource) -> None: + self._blocked_handles = blocked_handles + + self.list = async_to_streamed_response_wrapper( + blocked_handles.list, + ) + self.block = async_to_streamed_response_wrapper( + blocked_handles.block, + ) + self.unblock = async_to_streamed_response_wrapper( + blocked_handles.unblock, + ) diff --git a/src/linq/types/__init__.py b/src/linq/types/__init__.py index e5b78f2..57e5153 100644 --- a/src/linq/types/__init__.py +++ b/src/linq/types/__init__.py @@ -29,6 +29,7 @@ from .chat_update_params import ChatUpdateParams as ChatUpdateParams from .webhook_event_type import WebhookEventType as WebhookEventType from .reaction_event_base import ReactionEventBase as ReactionEventBase +from .blocked_handle_entry import BlockedHandleEntry as BlockedHandleEntry from .chat_create_response import ChatCreateResponse as ChatCreateResponse from .chat_update_response import ChatUpdateResponse as ChatUpdateResponse from .message_effect_param import MessageEffectParam as MessageEffectParam @@ -58,12 +59,14 @@ from .phone_number_list_response import PhoneNumberListResponse as PhoneNumberListResponse from .phone_number_update_params import PhoneNumberUpdateParams as PhoneNumberUpdateParams from .schemas_text_part_response import SchemasTextPartResponse as SchemasTextPartResponse +from .blocked_handle_block_params import BlockedHandleBlockParams as BlockedHandleBlockParams from .capability_check_RCS_params import CapabilityCheckRCSParams as CapabilityCheckRCSParams from .message_add_reaction_params import MessageAddReactionParams as MessageAddReactionParams from .payment_request_list_params import PaymentRequestListParams as PaymentRequestListParams from .schemas_media_part_response import SchemasMediaPartResponse as SchemasMediaPartResponse from .webhook_event_list_response import WebhookEventListResponse as WebhookEventListResponse from .attachment_retrieve_response import AttachmentRetrieveResponse as AttachmentRetrieveResponse +from .blocked_handle_list_response import BlockedHandleListResponse as BlockedHandleListResponse from .chat_send_voicememo_response import ChatSendVoicememoResponse as ChatSendVoicememoResponse from .contact_card_retrieve_params import ContactCardRetrieveParams as ContactCardRetrieveParams from .experience_retrieve_response import ExperienceRetrieveResponse as ExperienceRetrieveResponse @@ -73,6 +76,8 @@ from .payment_handle_verify_params import PaymentHandleVerifyParams as PaymentHandleVerifyParams from .phone_number_update_response import PhoneNumberUpdateResponse as PhoneNumberUpdateResponse from .reaction_added_webhook_event import ReactionAddedWebhookEvent as ReactionAddedWebhookEvent +from .blocked_handle_block_response import BlockedHandleBlockResponse as BlockedHandleBlockResponse +from .blocked_handle_unblock_params import BlockedHandleUnblockParams as BlockedHandleUnblockParams from .message_add_reaction_response import MessageAddReactionResponse as MessageAddReactionResponse from .payment_request_create_params import PaymentRequestCreateParams as PaymentRequestCreateParams from .payment_request_list_response import PaymentRequestListResponse as PaymentRequestListResponse diff --git a/src/linq/types/blocked_handle_block_params.py b/src/linq/types/blocked_handle_block_params.py new file mode 100644 index 0000000..a77d96c --- /dev/null +++ b/src/linq/types/blocked_handle_block_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BlockedHandleBlockParams"] + + +class BlockedHandleBlockParams(TypedDict, total=False): + handle: Required[str] + """ + The handle to block: an E.164 phone number, an email address, an SMS short code + (3-8 digits), or an alphanumeric sender ID. + """ + + reason: str + """Optional free-text note on why the handle was blocked""" diff --git a/src/linq/types/blocked_handle_block_response.py b/src/linq/types/blocked_handle_block_response.py new file mode 100644 index 0000000..e963152 --- /dev/null +++ b/src/linq/types/blocked_handle_block_response.py @@ -0,0 +1,10 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel +from .blocked_handle_entry import BlockedHandleEntry + +__all__ = ["BlockedHandleBlockResponse"] + + +class BlockedHandleBlockResponse(BaseModel): + blocked_handle: BlockedHandleEntry diff --git a/src/linq/types/blocked_handle_entry.py b/src/linq/types/blocked_handle_entry.py new file mode 100644 index 0000000..afe182b --- /dev/null +++ b/src/linq/types/blocked_handle_entry.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["BlockedHandleEntry"] + + +class BlockedHandleEntry(BaseModel): + blocked_at: datetime + """When the handle was blocked""" + + handle: str + """ + The blocked handle, normalized (E.164 phone, lowercased email, short code, or + sender ID) + """ + + reason: Optional[str] = None + """Optional note recorded when the handle was blocked""" diff --git a/src/linq/types/blocked_handle_list_response.py b/src/linq/types/blocked_handle_list_response.py new file mode 100644 index 0000000..2d1d23e --- /dev/null +++ b/src/linq/types/blocked_handle_list_response.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .blocked_handle_entry import BlockedHandleEntry + +__all__ = ["BlockedHandleListResponse"] + + +class BlockedHandleListResponse(BaseModel): + blocked_handles: List[BlockedHandleEntry] + """All handles blocked by the partner, newest first""" diff --git a/src/linq/types/blocked_handle_unblock_params.py b/src/linq/types/blocked_handle_unblock_params.py new file mode 100644 index 0000000..b27de4a --- /dev/null +++ b/src/linq/types/blocked_handle_unblock_params.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BlockedHandleUnblockParams"] + + +class BlockedHandleUnblockParams(TypedDict, total=False): + handle: Required[str] + """The handle to unblock""" diff --git a/tests/api_resources/test_blocked_handles.py b/tests/api_resources/test_blocked_handles.py new file mode 100644 index 0000000..4a1a617 --- /dev/null +++ b/tests/api_resources/test_blocked_handles.py @@ -0,0 +1,237 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from linq import LinqAPIV3, AsyncLinqAPIV3 +from linq.types import ( + BlockedHandleListResponse, + BlockedHandleBlockResponse, +) +from tests.utils import assert_matches_type + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestBlockedHandles: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: LinqAPIV3) -> None: + blocked_handle = client.blocked_handles.list() + assert_matches_type(BlockedHandleListResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: LinqAPIV3) -> None: + response = client.blocked_handles.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blocked_handle = response.parse() + assert_matches_type(BlockedHandleListResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: LinqAPIV3) -> None: + with client.blocked_handles.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blocked_handle = response.parse() + assert_matches_type(BlockedHandleListResponse, blocked_handle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_block(self, client: LinqAPIV3) -> None: + blocked_handle = client.blocked_handles.block( + handle="+12025551234", + ) + assert_matches_type(BlockedHandleBlockResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_block_with_all_params(self, client: LinqAPIV3) -> None: + blocked_handle = client.blocked_handles.block( + handle="+12025551234", + reason="spam", + ) + assert_matches_type(BlockedHandleBlockResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_block(self, client: LinqAPIV3) -> None: + response = client.blocked_handles.with_raw_response.block( + handle="+12025551234", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blocked_handle = response.parse() + assert_matches_type(BlockedHandleBlockResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_block(self, client: LinqAPIV3) -> None: + with client.blocked_handles.with_streaming_response.block( + handle="+12025551234", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blocked_handle = response.parse() + assert_matches_type(BlockedHandleBlockResponse, blocked_handle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_unblock(self, client: LinqAPIV3) -> None: + blocked_handle = client.blocked_handles.unblock( + handle="+12025551234", + ) + assert blocked_handle is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_unblock(self, client: LinqAPIV3) -> None: + response = client.blocked_handles.with_raw_response.unblock( + handle="+12025551234", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blocked_handle = response.parse() + assert blocked_handle is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_unblock(self, client: LinqAPIV3) -> None: + with client.blocked_handles.with_streaming_response.unblock( + handle="+12025551234", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blocked_handle = response.parse() + assert blocked_handle is None + + assert cast(Any, response.is_closed) is True + + +class TestAsyncBlockedHandles: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncLinqAPIV3) -> None: + blocked_handle = await async_client.blocked_handles.list() + assert_matches_type(BlockedHandleListResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.blocked_handles.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blocked_handle = await response.parse() + assert_matches_type(BlockedHandleListResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.blocked_handles.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blocked_handle = await response.parse() + assert_matches_type(BlockedHandleListResponse, blocked_handle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_block(self, async_client: AsyncLinqAPIV3) -> None: + blocked_handle = await async_client.blocked_handles.block( + handle="+12025551234", + ) + assert_matches_type(BlockedHandleBlockResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_block_with_all_params(self, async_client: AsyncLinqAPIV3) -> None: + blocked_handle = await async_client.blocked_handles.block( + handle="+12025551234", + reason="spam", + ) + assert_matches_type(BlockedHandleBlockResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_block(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.blocked_handles.with_raw_response.block( + handle="+12025551234", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blocked_handle = await response.parse() + assert_matches_type(BlockedHandleBlockResponse, blocked_handle, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_block(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.blocked_handles.with_streaming_response.block( + handle="+12025551234", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blocked_handle = await response.parse() + assert_matches_type(BlockedHandleBlockResponse, blocked_handle, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_unblock(self, async_client: AsyncLinqAPIV3) -> None: + blocked_handle = await async_client.blocked_handles.unblock( + handle="+12025551234", + ) + assert blocked_handle is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_unblock(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.blocked_handles.with_raw_response.unblock( + handle="+12025551234", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + blocked_handle = await response.parse() + assert blocked_handle is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_unblock(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.blocked_handles.with_streaming_response.unblock( + handle="+12025551234", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + blocked_handle = await response.parse() + assert blocked_handle is None + + assert cast(Any, response.is_closed) is True From fd77f1c421030fe9c719c0481ec592acb554d9b8 Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Mon, 10 Aug 2026 20:30:00 +0000 Subject: [PATCH 8/8] feat: regenerate SDKs from updated API spec --- src/linq/resources/messages/messages.py | 22 +++++++++ .../types/message_update_app_card_params.py | 45 ++++++++++++++++++- tests/api_resources/test_messages.py | 10 +++++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/linq/resources/messages/messages.py b/src/linq/resources/messages/messages.py index 601bd85..8fa2309 100644 --- a/src/linq/resources/messages/messages.py +++ b/src/linq/resources/messages/messages.py @@ -580,6 +580,7 @@ def update_app_card( message_id: str, *, layout: message_update_app_card_params.Layout, + action: message_update_app_card_params.Action | Omit = omit, fallback_text: str | Omit = omit, interactive: bool | Omit = omit, url: str | Omit = omit, @@ -630,6 +631,13 @@ def update_app_card( an image there is nothing to overlay — so setting either without `image_url` is rejected. + action: Invokes an action on an experience — a third party that renders inside Linq's + iMessage app. Linq resolves the recipient's connection, mints any session the + action needs, composes the card and sends it; none of that is visible to you. + + Call `GET /v3/experiences/{experience}` for the actions you may invoke and the + fields each accepts. + fallback_text: Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. @@ -645,6 +653,8 @@ def update_app_card( url: URL the recipient's app opens when they tap the updated card. + Mutually exclusive with `action` and `raw_payload_data`. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -660,6 +670,7 @@ def update_app_card( body=maybe_transform( { "layout": layout, + "action": action, "fallback_text": fallback_text, "interactive": interactive, "url": url, @@ -1210,6 +1221,7 @@ async def update_app_card( message_id: str, *, layout: message_update_app_card_params.Layout, + action: message_update_app_card_params.Action | Omit = omit, fallback_text: str | Omit = omit, interactive: bool | Omit = omit, url: str | Omit = omit, @@ -1260,6 +1272,13 @@ async def update_app_card( an image there is nothing to overlay — so setting either without `image_url` is rejected. + action: Invokes an action on an experience — a third party that renders inside Linq's + iMessage app. Linq resolves the recipient's connection, mints any session the + action needs, composes the card and sends it; none of that is visible to you. + + Call `GET /v3/experiences/{experience}` for the actions you may invoke and the + fields each accepts. + fallback_text: Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. @@ -1275,6 +1294,8 @@ async def update_app_card( url: URL the recipient's app opens when they tap the updated card. + Mutually exclusive with `action` and `raw_payload_data`. + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -1290,6 +1311,7 @@ async def update_app_card( body=await async_maybe_transform( { "layout": layout, + "action": action, "fallback_text": fallback_text, "interactive": interactive, "url": url, diff --git a/src/linq/types/message_update_app_card_params.py b/src/linq/types/message_update_app_card_params.py index d01337d..32bd023 100644 --- a/src/linq/types/message_update_app_card_params.py +++ b/src/linq/types/message_update_app_card_params.py @@ -2,9 +2,10 @@ from __future__ import annotations +from typing import Dict from typing_extensions import Required, TypedDict -__all__ = ["MessageUpdateAppCardParams", "Layout"] +__all__ = ["MessageUpdateAppCardParams", "Layout", "Action"] class MessageUpdateAppCardParams(TypedDict, total=False): @@ -27,6 +28,16 @@ class MessageUpdateAppCardParams(TypedDict, total=False): rejected. """ + action: Action + """ + Invokes an action on an experience — a third party that renders inside Linq's + iMessage app. Linq resolves the recipient's connection, mints any session the + action needs, composes the card and sends it; none of that is visible to you. + + Call `GET /v3/experiences/{experience}` for the actions you may invoke and the + fields each accepts. + """ + fallback_text: str """Text shown on surfaces that cannot render the card (notifications, lock screen). @@ -47,7 +58,10 @@ class MessageUpdateAppCardParams(TypedDict, total=False): """ url: str - """URL the recipient's app opens when they tap the updated card.""" + """URL the recipient's app opens when they tap the updated card. + + Mutually exclusive with `action` and `raw_payload_data`. + """ class Layout(TypedDict, total=False): @@ -100,3 +114,30 @@ class Layout(TypedDict, total=False): trailing_subcaption: str """Label shown below `trailing_caption`, on the right.""" + + +class Action(TypedDict, total=False): + """ + Invokes an action on an experience — a third party that renders inside + Linq's iMessage app. Linq resolves the recipient's connection, mints any + session the action needs, composes the card and sends it; none of that + is visible to you. + + Call `GET /v3/experiences/{experience}` for the actions you may invoke + and the fields each accepts. + """ + + action: Required[str] + """Which of its actions, e.g. `attach_card`.""" + + experience: Required[str] + """The experience to invoke, e.g. `agentcard`.""" + + params: Dict[str, object] + """Values for the fields this action exposes. + + Keys are exactly the field names listed for the action — no mapping, no nesting. + + Display copy only, except a `url`-type field — that value sets the destination, + and must be an absolute `https` URL. + """ diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py index 51beb7e..8fe3d38 100644 --- a/tests/api_resources/test_messages.py +++ b/tests/api_resources/test_messages.py @@ -387,6 +387,11 @@ def test_method_update_app_card_with_all_params(self, client: LinqAPIV3) -> None "trailing_caption": "2 min", "trailing_subcaption": "expires", }, + action={ + "action": "attach_card", + "experience": "agentcard", + "params": {"foo": "bar"}, + }, fallback_text="Score update", interactive=True, url="https://app.example.com/card?game=7f3a&move=2", @@ -800,6 +805,11 @@ async def test_method_update_app_card_with_all_params(self, async_client: AsyncL "trailing_caption": "2 min", "trailing_subcaption": "expires", }, + action={ + "action": "attach_card", + "experience": "agentcard", + "params": {"foo": "bar"}, + }, fallback_text="Score update", interactive=True, url="https://app.example.com/card?game=7f3a&move=2",