From 5846090e780dd1a941c0573d5123cc491d4d4cf1 Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Mon, 6 Jul 2026 20:26:12 +0000 Subject: [PATCH] docs: clarify phone line reputation status descriptions --- .stats.yml | 2 +- api.md | 26 +- src/linq/_client.py | 354 +++++++++++- src/linq/resources/__init__.py | 14 + src/linq/resources/attachments.py | 4 +- src/linq/resources/available_number.py | 221 ++++++++ src/linq/resources/chats/chats.py | 240 +++++++++ src/linq/resources/chats/location.py | 34 +- src/linq/resources/chats/messages.py | 80 +++ src/linq/resources/contact_card.py | 6 +- src/linq/resources/messages.py | 509 +++++++++++++++++- src/linq/resources/phone_numbers.py | 103 ++++ src/linq/types/__init__.py | 8 + .../types/available_number_retrieve_params.py | 18 + .../available_number_retrieve_response.py | 20 + src/linq/types/chats/sent_message.py | 54 +- src/linq/types/contact_card_create_params.py | 5 +- src/linq/types/message.py | 54 +- src/linq/types/message_content_param.py | 78 ++- src/linq/types/message_create_params.py | 55 ++ src/linq/types/message_create_response.py | 67 +++ src/linq/types/message_event_v2.py | 2 +- .../types/message_update_app_card_params.py | 102 ++++ .../types/message_update_app_card_response.py | 16 + src/linq/types/phone_number_list_response.py | 52 +- ...one_number_status_updated_webhook_event.py | 24 +- src/linq/types/phone_number_update_params.py | 13 + .../types/phone_number_update_response.py | 18 + tests/api_resources/test_available_number.py | 96 ++++ tests/api_resources/test_messages.py | 330 ++++++++++++ tests/api_resources/test_phone_numbers.py | 94 +++- uv.lock | 2 +- 32 files changed, 2594 insertions(+), 107 deletions(-) create mode 100644 src/linq/resources/available_number.py create mode 100644 src/linq/types/available_number_retrieve_params.py create mode 100644 src/linq/types/available_number_retrieve_response.py create mode 100644 src/linq/types/message_create_params.py create mode 100644 src/linq/types/message_create_response.py create mode 100644 src/linq/types/message_update_app_card_params.py create mode 100644 src/linq/types/message_update_app_card_response.py create mode 100644 src/linq/types/phone_number_update_params.py create mode 100644 src/linq/types/phone_number_update_response.py create mode 100644 tests/api_resources/test_available_number.py diff --git a/.stats.yml b/.stats.yml index 7982133..1fd2991 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1 @@ -configured_endpoints: 37 +configured_endpoints: 41 diff --git a/api.md b/api.md index cbb519f..31bb7ea 100644 --- a/api.md +++ b/api.md @@ -93,16 +93,25 @@ Methods: Types: ```python -from linq.types import Message, MessageEffect, ReplyTo, MessageAddReactionResponse +from linq.types import ( + Message, + MessageEffect, + ReplyTo, + MessageCreateResponse, + MessageAddReactionResponse, + MessageUpdateAppCardResponse, +) ``` Methods: +- client.messages.create(\*\*params) -> MessageCreateResponse - client.messages.retrieve(message_id) -> Message - client.messages.update(message_id, \*\*params) -> Message - client.messages.delete(message_id) -> None - client.messages.add_reaction(message_id, \*\*params) -> MessageAddReactionResponse - client.messages.list_messages_thread(message_id, \*\*params) -> SyncListMessagesPagination[Message] +- client.messages.update_app_card(message_id, \*\*params) -> MessageUpdateAppCardResponse # Attachments @@ -135,13 +144,26 @@ Methods: Types: ```python -from linq.types import PhoneNumberListResponse +from linq.types import PhoneNumberUpdateResponse, PhoneNumberListResponse ``` Methods: +- client.phone_numbers.update(phone_number_id, \*\*params) -> PhoneNumberUpdateResponse - client.phone_numbers.list() -> PhoneNumberListResponse +# AvailableNumber + +Types: + +```python +from linq.types import AvailableNumberRetrieveResponse +``` + +Methods: + +- client.available_number.retrieve(\*\*params) -> AvailableNumberRetrieveResponse + # WebhookEvents Types: diff --git a/src/linq/_client.py b/src/linq/_client.py index 7d3b19f..45286ad 100644 --- a/src/linq/_client.py +++ b/src/linq/_client.py @@ -45,6 +45,7 @@ phonenumbers, phone_numbers, webhook_events, + available_number, webhook_subscriptions, ) from .resources.messages import MessagesResource, AsyncMessagesResource @@ -56,6 +57,7 @@ from .resources.phonenumbers import PhonenumbersResource, AsyncPhonenumbersResource from .resources.phone_numbers import PhoneNumbersResource, AsyncPhoneNumbersResource from .resources.webhook_events import WebhookEventsResource, AsyncWebhookEventsResource + from .resources.available_number import AvailableNumberResource, AsyncAvailableNumberResource from .resources.webhook_subscriptions import WebhookSubscriptionsResource, AsyncWebhookSubscriptionsResource __all__ = [ @@ -168,6 +170,46 @@ def messages(self) -> MessagesResource: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ from .resources.messages import MessagesResource @@ -339,7 +381,7 @@ def attachments(self) -> AttachmentsResource: |---|---|---| | Attachment bytes | Retained until you `DELETE` | **Auto-removed after 1 day**, also removable via `DELETE` | | Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE` | Removed alongside the bytes | - | Message body & parts | Retained per message-retention policy | Retained per message-retention policy | + | Message body & parts | Retained per message-retention policy | Retained per message-retention policy — unless the line also has **ephemeral messages** enabled (see the Messages page), in which case the message and its parts are deleted 24 hours after creation | | Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy | **In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption). @@ -387,6 +429,20 @@ def phone_numbers(self) -> PhoneNumbersResource: return PhoneNumbersResource(self) + @cached_property + def available_number(self) -> AvailableNumberResource: + """Phone Numbers represent the phone numbers assigned to your partner account. + + Use the list phone numbers endpoint to discover which phone numbers are available + for sending messages. + + When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers + in the `from` field. + """ + from .resources.available_number import AvailableNumberResource + + return AvailableNumberResource(self) + @cached_property def webhook_events(self) -> WebhookEventsResource: """ @@ -698,9 +754,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]: @@ -901,6 +959,46 @@ def messages(self) -> AsyncMessagesResource: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ from .resources.messages import AsyncMessagesResource @@ -1072,7 +1170,7 @@ def attachments(self) -> AsyncAttachmentsResource: |---|---|---| | Attachment bytes | Retained until you `DELETE` | **Auto-removed after 1 day**, also removable via `DELETE` | | Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE` | Removed alongside the bytes | - | Message body & parts | Retained per message-retention policy | Retained per message-retention policy | + | Message body & parts | Retained per message-retention policy | Retained per message-retention policy — unless the line also has **ephemeral messages** enabled (see the Messages page), in which case the message and its parts are deleted 24 hours after creation | | Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy | **In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption). @@ -1120,6 +1218,20 @@ def phone_numbers(self) -> AsyncPhoneNumbersResource: return AsyncPhoneNumbersResource(self) + @cached_property + def available_number(self) -> AsyncAvailableNumberResource: + """Phone Numbers represent the phone numbers assigned to your partner account. + + Use the list phone numbers endpoint to discover which phone numbers are available + for sending messages. + + When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers + in the `from` field. + """ + from .resources.available_number import AsyncAvailableNumberResource + + return AsyncAvailableNumberResource(self) + @cached_property def webhook_events(self) -> AsyncWebhookEventsResource: """ @@ -1431,9 +1543,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]: @@ -1568,6 +1682,46 @@ def messages(self) -> messages.MessagesResourceWithRawResponse: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ from .resources.messages import MessagesResourceWithRawResponse @@ -1739,7 +1893,7 @@ def attachments(self) -> attachments.AttachmentsResourceWithRawResponse: |---|---|---| | Attachment bytes | Retained until you `DELETE` | **Auto-removed after 1 day**, also removable via `DELETE` | | Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE` | Removed alongside the bytes | - | Message body & parts | Retained per message-retention policy | Retained per message-retention policy | + | Message body & parts | Retained per message-retention policy | Retained per message-retention policy — unless the line also has **ephemeral messages** enabled (see the Messages page), in which case the message and its parts are deleted 24 hours after creation | | Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy | **In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption). @@ -1787,6 +1941,20 @@ def phone_numbers(self) -> phone_numbers.PhoneNumbersResourceWithRawResponse: return PhoneNumbersResourceWithRawResponse(self._client.phone_numbers) + @cached_property + def available_number(self) -> available_number.AvailableNumberResourceWithRawResponse: + """Phone Numbers represent the phone numbers assigned to your partner account. + + Use the list phone numbers endpoint to discover which phone numbers are available + for sending messages. + + When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers + in the `from` field. + """ + from .resources.available_number import AvailableNumberResourceWithRawResponse + + return AvailableNumberResourceWithRawResponse(self._client.available_number) + @cached_property def webhook_events(self) -> webhook_events.WebhookEventsResourceWithRawResponse: """ @@ -2110,6 +2278,46 @@ def messages(self) -> messages.AsyncMessagesResourceWithRawResponse: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ from .resources.messages import AsyncMessagesResourceWithRawResponse @@ -2281,7 +2489,7 @@ def attachments(self) -> attachments.AsyncAttachmentsResourceWithRawResponse: |---|---|---| | Attachment bytes | Retained until you `DELETE` | **Auto-removed after 1 day**, also removable via `DELETE` | | Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE` | Removed alongside the bytes | - | Message body & parts | Retained per message-retention policy | Retained per message-retention policy | + | Message body & parts | Retained per message-retention policy | Retained per message-retention policy — unless the line also has **ephemeral messages** enabled (see the Messages page), in which case the message and its parts are deleted 24 hours after creation | | Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy | **In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption). @@ -2329,6 +2537,20 @@ def phone_numbers(self) -> phone_numbers.AsyncPhoneNumbersResourceWithRawRespons return AsyncPhoneNumbersResourceWithRawResponse(self._client.phone_numbers) + @cached_property + def available_number(self) -> available_number.AsyncAvailableNumberResourceWithRawResponse: + """Phone Numbers represent the phone numbers assigned to your partner account. + + Use the list phone numbers endpoint to discover which phone numbers are available + for sending messages. + + When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers + in the `from` field. + """ + from .resources.available_number import AsyncAvailableNumberResourceWithRawResponse + + return AsyncAvailableNumberResourceWithRawResponse(self._client.available_number) + @cached_property def webhook_events(self) -> webhook_events.AsyncWebhookEventsResourceWithRawResponse: """ @@ -2652,6 +2874,46 @@ def messages(self) -> messages.MessagesResourceWithStreamingResponse: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ from .resources.messages import MessagesResourceWithStreamingResponse @@ -2823,7 +3085,7 @@ def attachments(self) -> attachments.AttachmentsResourceWithStreamingResponse: |---|---|---| | Attachment bytes | Retained until you `DELETE` | **Auto-removed after 1 day**, also removable via `DELETE` | | Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE` | Removed alongside the bytes | - | Message body & parts | Retained per message-retention policy | Retained per message-retention policy | + | Message body & parts | Retained per message-retention policy | Retained per message-retention policy — unless the line also has **ephemeral messages** enabled (see the Messages page), in which case the message and its parts are deleted 24 hours after creation | | Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy | **In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption). @@ -2871,6 +3133,20 @@ def phone_numbers(self) -> phone_numbers.PhoneNumbersResourceWithStreamingRespon return PhoneNumbersResourceWithStreamingResponse(self._client.phone_numbers) + @cached_property + def available_number(self) -> available_number.AvailableNumberResourceWithStreamingResponse: + """Phone Numbers represent the phone numbers assigned to your partner account. + + Use the list phone numbers endpoint to discover which phone numbers are available + for sending messages. + + When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers + in the `from` field. + """ + from .resources.available_number import AvailableNumberResourceWithStreamingResponse + + return AvailableNumberResourceWithStreamingResponse(self._client.available_number) + @cached_property def webhook_events(self) -> webhook_events.WebhookEventsResourceWithStreamingResponse: """ @@ -3194,6 +3470,46 @@ def messages(self) -> messages.AsyncMessagesResourceWithStreamingResponse: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ from .resources.messages import AsyncMessagesResourceWithStreamingResponse @@ -3365,7 +3681,7 @@ def attachments(self) -> attachments.AsyncAttachmentsResourceWithStreamingRespon |---|---|---| | Attachment bytes | Retained until you `DELETE` | **Auto-removed after 1 day**, also removable via `DELETE` | | Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE` | Removed alongside the bytes | - | Message body & parts | Retained per message-retention policy | Retained per message-retention policy | + | Message body & parts | Retained per message-retention policy | Retained per message-retention policy — unless the line also has **ephemeral messages** enabled (see the Messages page), in which case the message and its parts are deleted 24 hours after creation | | Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy | **In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption). @@ -3413,6 +3729,20 @@ def phone_numbers(self) -> phone_numbers.AsyncPhoneNumbersResourceWithStreamingR return AsyncPhoneNumbersResourceWithStreamingResponse(self._client.phone_numbers) + @cached_property + def available_number(self) -> available_number.AsyncAvailableNumberResourceWithStreamingResponse: + """Phone Numbers represent the phone numbers assigned to your partner account. + + Use the list phone numbers endpoint to discover which phone numbers are available + for sending messages. + + When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers + in the `from` field. + """ + from .resources.available_number import AsyncAvailableNumberResourceWithStreamingResponse + + return AsyncAvailableNumberResourceWithStreamingResponse(self._client.available_number) + @cached_property def webhook_events(self) -> webhook_events.AsyncWebhookEventsResourceWithStreamingResponse: """ diff --git a/src/linq/resources/__init__.py b/src/linq/resources/__init__.py index a3e8223..00e6eb8 100644 --- a/src/linq/resources/__init__.py +++ b/src/linq/resources/__init__.py @@ -65,6 +65,14 @@ WebhookEventsResourceWithStreamingResponse, AsyncWebhookEventsResourceWithStreamingResponse, ) +from .available_number import ( + AvailableNumberResource, + AsyncAvailableNumberResource, + AvailableNumberResourceWithRawResponse, + AsyncAvailableNumberResourceWithRawResponse, + AvailableNumberResourceWithStreamingResponse, + AsyncAvailableNumberResourceWithStreamingResponse, +) from .webhook_subscriptions import ( WebhookSubscriptionsResource, AsyncWebhookSubscriptionsResource, @@ -105,6 +113,12 @@ "AsyncPhoneNumbersResourceWithRawResponse", "PhoneNumbersResourceWithStreamingResponse", "AsyncPhoneNumbersResourceWithStreamingResponse", + "AvailableNumberResource", + "AsyncAvailableNumberResource", + "AvailableNumberResourceWithRawResponse", + "AsyncAvailableNumberResourceWithRawResponse", + "AvailableNumberResourceWithStreamingResponse", + "AsyncAvailableNumberResourceWithStreamingResponse", "WebhookEventsResource", "AsyncWebhookEventsResource", "WebhookEventsResourceWithRawResponse", diff --git a/src/linq/resources/attachments.py b/src/linq/resources/attachments.py index faf3822..c4c49d9 100644 --- a/src/linq/resources/attachments.py +++ b/src/linq/resources/attachments.py @@ -188,7 +188,7 @@ class AttachmentsResource(SyncAPIResource): |---|---|---| | Attachment bytes | Retained until you `DELETE` | **Auto-removed after 1 day**, also removable via `DELETE` | | Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE` | Removed alongside the bytes | - | Message body & parts | Retained per message-retention policy | Retained per message-retention policy | + | Message body & parts | Retained per message-retention policy | Retained per message-retention policy — unless the line also has **ephemeral messages** enabled (see the Messages page), in which case the message and its parts are deleted 24 hours after creation | | Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy | **In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption). @@ -615,7 +615,7 @@ class AsyncAttachmentsResource(AsyncAPIResource): |---|---|---| | Attachment bytes | Retained until you `DELETE` | **Auto-removed after 1 day**, also removable via `DELETE` | | Attachment metadata (id, filename, mime type, size) | Retained until you `DELETE` | Removed alongside the bytes | - | Message body & parts | Retained per message-retention policy | Retained per message-retention policy | + | Message body & parts | Retained per message-retention policy | Retained per message-retention policy — unless the line also has **ephemeral messages** enabled (see the Messages page), in which case the message and its parts are deleted 24 hours after creation | | Audit log of deletions | Retained per platform retention policy | Retained per platform retention policy | **In transit:** TLS 1.2+ everywhere. **At rest:** AES-256 (server-side encryption). diff --git a/src/linq/resources/available_number.py b/src/linq/resources/available_number.py new file mode 100644 index 0000000..0fb879a --- /dev/null +++ b/src/linq/resources/available_number.py @@ -0,0 +1,221 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import available_number_retrieve_params +from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, 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.available_number_retrieve_response import AvailableNumberRetrieveResponse + +__all__ = ["AvailableNumberResource", "AsyncAvailableNumberResource"] + + +class AvailableNumberResource(SyncAPIResource): + """Phone Numbers represent the phone numbers assigned to your partner account. + + Use the list phone numbers endpoint to discover which phone numbers are available + for sending messages. + + When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers + in the `from` field. + """ + + @cached_property + def with_raw_response(self) -> AvailableNumberResourceWithRawResponse: + """ + 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 AvailableNumberResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AvailableNumberResourceWithStreamingResponse: + """ + 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 AvailableNumberResourceWithStreamingResponse(self) + + def retrieve( + self, + *, + to: SequenceNotStr[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, + ) -> AvailableNumberRetrieveResponse: + """ + Returns the best available line (E.164) to send from, applying smart number + assignment. Optionally pass `to` recipients to make the choice "sticky" — + reusing the line an existing chat with those recipients is already on. Without + `to`, the best healthy line is chosen. + + This is advisory: it does not reserve the line or change selection state. Pass + the returned `phone_number` as `from` when you create the chat to guarantee the + same line. + + Also returns `vcf_url`: a time-limited link to a vCard (`.vcf`) for the chosen + line, carrying its contact card (name/photo) with the chosen number as the + primary `TEL` and the partner's other healthy lines as backups. Share it with + recipients so they can save the line as a contact. + + Args: + to: Recipient handles (E.164 or email) the message is destined for. When provided, + an existing chat with these recipients makes the choice sticky. Repeat the + parameter for multiple recipients. + + 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._get( + "/v3/available_number", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform({"to": to}, available_number_retrieve_params.AvailableNumberRetrieveParams), + ), + cast_to=AvailableNumberRetrieveResponse, + ) + + +class AsyncAvailableNumberResource(AsyncAPIResource): + """Phone Numbers represent the phone numbers assigned to your partner account. + + Use the list phone numbers endpoint to discover which phone numbers are available + for sending messages. + + When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers + in the `from` field. + """ + + @cached_property + def with_raw_response(self) -> AsyncAvailableNumberResourceWithRawResponse: + """ + 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 AsyncAvailableNumberResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAvailableNumberResourceWithStreamingResponse: + """ + 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 AsyncAvailableNumberResourceWithStreamingResponse(self) + + async def retrieve( + self, + *, + to: SequenceNotStr[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, + ) -> AvailableNumberRetrieveResponse: + """ + Returns the best available line (E.164) to send from, applying smart number + assignment. Optionally pass `to` recipients to make the choice "sticky" — + reusing the line an existing chat with those recipients is already on. Without + `to`, the best healthy line is chosen. + + This is advisory: it does not reserve the line or change selection state. Pass + the returned `phone_number` as `from` when you create the chat to guarantee the + same line. + + Also returns `vcf_url`: a time-limited link to a vCard (`.vcf`) for the chosen + line, carrying its contact card (name/photo) with the chosen number as the + primary `TEL` and the partner's other healthy lines as backups. Share it with + recipients so they can save the line as a contact. + + Args: + to: Recipient handles (E.164 or email) the message is destined for. When provided, + an existing chat with these recipients makes the choice sticky. Repeat the + parameter for multiple recipients. + + 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._get( + "/v3/available_number", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + {"to": to}, available_number_retrieve_params.AvailableNumberRetrieveParams + ), + ), + cast_to=AvailableNumberRetrieveResponse, + ) + + +class AvailableNumberResourceWithRawResponse: + def __init__(self, available_number: AvailableNumberResource) -> None: + self._available_number = available_number + + self.retrieve = to_raw_response_wrapper( + available_number.retrieve, + ) + + +class AsyncAvailableNumberResourceWithRawResponse: + def __init__(self, available_number: AsyncAvailableNumberResource) -> None: + self._available_number = available_number + + self.retrieve = async_to_raw_response_wrapper( + available_number.retrieve, + ) + + +class AvailableNumberResourceWithStreamingResponse: + def __init__(self, available_number: AvailableNumberResource) -> None: + self._available_number = available_number + + self.retrieve = to_streamed_response_wrapper( + available_number.retrieve, + ) + + +class AsyncAvailableNumberResourceWithStreamingResponse: + def __init__(self, available_number: AsyncAvailableNumberResource) -> None: + self._available_number = available_number + + self.retrieve = async_to_streamed_response_wrapper( + available_number.retrieve, + ) diff --git a/src/linq/resources/chats/chats.py b/src/linq/resources/chats/chats.py index 8d66c06..20022f4 100644 --- a/src/linq/resources/chats/chats.py +++ b/src/linq/resources/chats/chats.py @@ -127,6 +127,46 @@ def messages(self) -> MessagesResource: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ return MessagesResource(self._client) @@ -679,6 +719,46 @@ def messages(self) -> AsyncMessagesResource: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ return AsyncMessagesResource(self._client) @@ -1259,6 +1339,46 @@ def messages(self) -> MessagesResourceWithRawResponse: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ return MessagesResourceWithRawResponse(self._chats.messages) @@ -1367,6 +1487,46 @@ def messages(self) -> AsyncMessagesResourceWithRawResponse: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ return AsyncMessagesResourceWithRawResponse(self._chats.messages) @@ -1475,6 +1635,46 @@ def messages(self) -> MessagesResourceWithStreamingResponse: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ return MessagesResourceWithStreamingResponse(self._chats.messages) @@ -1583,6 +1783,46 @@ def messages(self) -> AsyncMessagesResourceWithStreamingResponse: **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ return AsyncMessagesResourceWithStreamingResponse(self._chats.messages) diff --git a/src/linq/resources/chats/location.py b/src/linq/resources/chats/location.py index 4e051d7..d2dc409 100644 --- a/src/linq/resources/chats/location.py +++ b/src/linq/resources/chats/location.py @@ -65,16 +65,17 @@ def retrieve( """ Retrieve the current location for contacts sharing with you in a chat. - Returns a [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) - `FeatureCollection` with a `Feature` for each participant actively sharing their - location. + The response is wrapped in the standard `{ "success": true, "data": ... }` + envelope — the body is **not** a bare GeoJSON document. `data` is a + [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) `FeatureCollection` + with a `Feature` for each participant actively sharing their location. - Works for both 1:1 and group chats. In group chats, returns a separate feature - for each participant who is sharing. Each feature's `properties.handle` - identifies the user. + Works for both 1:1 and group chats. In group chats, `data.features` contains a + separate feature for each participant who is sharing. Each feature's + `properties.handle` identifies the user. - Returns an empty `features` array if no one is sharing or no location data is - available yet. + Returns an empty `data.features` array if no one is sharing or no location data + is available yet. Args: extra_headers: Send extra headers @@ -180,16 +181,17 @@ async def retrieve( """ Retrieve the current location for contacts sharing with you in a chat. - Returns a [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) - `FeatureCollection` with a `Feature` for each participant actively sharing their - location. + The response is wrapped in the standard `{ "success": true, "data": ... }` + envelope — the body is **not** a bare GeoJSON document. `data` is a + [GeoJSON](https://datatracker.ietf.org/doc/html/rfc7946) `FeatureCollection` + with a `Feature` for each participant actively sharing their location. - Works for both 1:1 and group chats. In group chats, returns a separate feature - for each participant who is sharing. Each feature's `properties.handle` - identifies the user. + Works for both 1:1 and group chats. In group chats, `data.features` contains a + separate feature for each participant who is sharing. Each feature's + `properties.handle` identifies the user. - Returns an empty `features` array if no one is sharing or no location data is - available yet. + Returns an empty `data.features` array if no one is sharing or no location data + is available yet. Args: extra_headers: Send extra headers diff --git a/src/linq/resources/chats/messages.py b/src/linq/resources/chats/messages.py index 97d031c..f993eb1 100644 --- a/src/linq/resources/chats/messages.py +++ b/src/linq/resources/chats/messages.py @@ -43,6 +43,46 @@ class MessagesResource(SyncAPIResource): **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ @cached_property @@ -215,6 +255,46 @@ class AsyncMessagesResource(AsyncAPIResource): **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ @cached_property diff --git a/src/linq/resources/contact_card.py b/src/linq/resources/contact_card.py index 2262174..c1e2e6e 100644 --- a/src/linq/resources/contact_card.py +++ b/src/linq/resources/contact_card.py @@ -83,8 +83,7 @@ def create( phone_number: E.164 phone number to associate the contact card with - image_url: URL of the profile image to rehost on the CDN. Only re-uploaded when a new value - is provided. + image_url: Profile image URL for the contact card. last_name: Last name for the contact card. Optional. @@ -278,8 +277,7 @@ async def create( phone_number: E.164 phone number to associate the contact card with - image_url: URL of the profile image to rehost on the CDN. Only re-uploaded when a new value - is provided. + image_url: Profile image URL for the contact card. last_name: Last name for the contact card. Optional. diff --git a/src/linq/resources/messages.py b/src/linq/resources/messages.py index 01bcd82..f2490ab 100644 --- a/src/linq/resources/messages.py +++ b/src/linq/resources/messages.py @@ -6,9 +6,15 @@ import httpx -from ..types import message_update_params, message_add_reaction_params, message_list_messages_thread_params -from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given -from .._utils import path_template, maybe_transform, async_maybe_transform +from ..types import ( + message_create_params, + message_update_params, + message_add_reaction_params, + message_update_app_card_params, + message_list_messages_thread_params, +) +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, SequenceNotStr, omit, not_given +from .._utils import path_template, maybe_transform, strip_not_given, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -21,7 +27,10 @@ from .._base_client import AsyncPaginator, make_request_options from ..types.message import Message from ..types.shared.reaction_type import ReactionType +from ..types.message_content_param import MessageContentParam +from ..types.message_create_response import MessageCreateResponse from ..types.message_add_reaction_response import MessageAddReactionResponse +from ..types.message_update_app_card_response import MessageUpdateAppCardResponse __all__ = ["MessagesResource", "AsyncMessagesResource"] @@ -45,6 +54,46 @@ class MessagesResource(SyncAPIResource): **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ @cached_property @@ -66,6 +115,104 @@ def with_streaming_response(self) -> MessagesResourceWithStreamingResponse: """ return MessagesResourceWithStreamingResponse(self) + def create( + self, + *, + message: MessageContentParam, + to: SequenceNotStr[str], + continuation_message: message_create_params.ContinuationMessage | Omit = omit, + idempotency_key: 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, + ) -> MessageCreateResponse: + """ + Send a message to one or more recipients **without supplying a `from` number**. + Linq resolves both the sending line and the target chat for you, then returns + exactly which line was used, which chat the message landed in, whether a new + chat was created, and every resulting message id. + + This fuses "create chat" and "send message" behind a single message-centric + resource. Provide only the recipients (`to`) and the `message`; the platform + decides the rest. + + ## How the from-number and chat are chosen + + - **Reuse** — if a chat with exactly these recipients already exists and the + line it lives on is healthy, the message is sent into that chat on its + existing line (`from_selection.reason = reused_active_chat`). + - **New** — if no such chat exists, a new chat is created on the best available + line (`from_selection.reason = new_best_number`). + - **Failover** — if a matching chat exists but its line has been flagged, a + **new** chat is created on a fresh best line and the flagged chat is abandoned + (`from_selection.reason = failover_flagged`, `previous_chat_id` set). If you + supply `continuation_message`, that text is sent as the single message INSTEAD + of `message` (useful as a fresh-number-appropriate opener). Exactly one + message is sent either way. + + Recipients (`to`) are an order-independent set: a single handle is a direct + chat, multiple handles a group chat. + + ## Differences from POST /v3/chats + + - The first message **may contain a link** (including for a newly created chat). + Note: sending a link as the very first message on a freshly selected line can + elevate that line's flagging risk — it is allowed, not recommended. + - Voice memos are **not** supported here. To send an iMessage voice-memo bubble, + use `POST /v3/chats/{chatId}/voicememo` with a known chat id. + + ## Service preference, effects, decorations + + Set `message.preferred_service` (`iMessage` | `RCS` | `SMS`), `message.effect`, + and per-part `text_decorations` exactly as on the other send endpoints. + + Always responds `202 Accepted` — chat creation is incidental to the send. + + Args: + message: Message content container. Groups all message-related fields together, + separating the "what" (message content) from the "where" (routing fields like + from/to). + + to: Recipient handles (E.164 phone numbers or email addresses). One handle is a + direct chat; multiple handles a group chat. Order-independent — the set + identifies the chat. + + continuation_message: Text-only fallback that **replaces** `message` ONLY on the failover branch — + when a chat with these recipients already existed but its line was flagged, so a + new chat is created on a fresh line. On that branch this text is sent as the + single message instead of `message` (the recipient is on a new number, so you + typically want a fresh-number-appropriate opener rather than the original + content). Ignored otherwise (a healthy reuse, or genuine first contact). Carries + no parts, media, or effects — exactly one message is ever sent. + + 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 = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return self._post( + "/v3/messages", + body=maybe_transform( + { + "message": message, + "to": to, + "continuation_message": continuation_message, + }, + message_create_params.MessageCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageCreateResponse, + ) + def retrieve( self, message_id: str, @@ -316,6 +463,103 @@ def list_messages_thread( model=Message, ) + def update_app_card( + self, + message_id: str, + *, + layout: message_update_app_card_params.Layout, + fallback_text: str | Omit = omit, + interactive: bool | Omit = omit, + url: 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, + ) -> MessageUpdateAppCardResponse: + """ + Replaces a previously delivered `imessage_app` card on the recipient's screen + with new content, instead of posting a new bubble (like a game move redrawing + the board). + + The update is delivered as a **new message** with its own id and delivery + lifecycle (`message.sent` / `message.delivered` / `message.failed` webhooks fire + for the new id). To update the card again, reference the message id returned by + this call. + + Constraints: + + - The referenced message must be an `imessage_app` card sent by you (`400` + otherwise — inbound cards cannot be updated). + - The referenced card must already be delivered (`409` otherwise — retry after + the `message.delivered` webhook for it). + - The app identity (`team_id`, `bundle_id`, name) is inherited from the original + card and cannot change; only `url`, `fallback_text`, and `layout` are + replaced. + - iMessage-only, like all app cards. + - Concurrent updates against the same card are not serialized server-side; the + last one delivered wins on the recipient's screen. Serialize updates by always + referencing the message id returned by the previous call. + + Args: + layout: Visible layout of the card. At least one of `caption`, `subcaption`, + `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise + the card renders as an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders + on the recipient's card whether or not they have your app installed. The small + icon beside the caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title + bold, subtitle beneath it). They only appear when `image_url` is set — without + an image there is nothing to overlay — so setting either without `image_url` is + rejected. + + fallback_text: Text shown on surfaces that cannot render the card (notifications, lock screen). + Defaults to the caption when omitted. + + interactive: Whether the updated card renders as your app's interactive balloon for + recipients who have your iMessage app installed. `true` (default) lets your + installed extension draw its live view; `false` always shows the static `layout` + card. Recipients without your app always see the static card regardless of this + flag. + + Defaults to `true` when omitted — it is **not** inherited from the original + card. To keep a card static across updates, re-send `interactive: false` on each + update. + + url: URL the recipient's app opens when they tap the updated card. + + 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 + """ + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return self._post( + path_template("/v3/messages/{message_id}/update", message_id=message_id), + body=maybe_transform( + { + "layout": layout, + "fallback_text": fallback_text, + "interactive": interactive, + "url": url, + }, + message_update_app_card_params.MessageUpdateAppCardParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageUpdateAppCardResponse, + ) + class AsyncMessagesResource(AsyncAPIResource): """Messages are individual communications within a chat thread. @@ -336,6 +580,46 @@ class AsyncMessagesResource(AsyncAPIResource): **Limitations:** - A `link` part cannot be combined with other parts in the same message. - Maximum URL length: 2,048 characters. + + ## Ephemeral Messages (Privacy Tier) + + For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + + You can request it at two scopes: + + | Scope | Effect | + |---|---| + | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + + **Behavioral differences vs the standard default:** + + | Aspect | Standard | Ephemeral | + |---|---|---| + | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + | Cross-partner isolation | Enforced | Enforced | + + **How the 24-hour window works:** + + - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + + **What you observe:** + + - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + + **When to choose ephemeral:** + + - You have a compliance requirement that the platform must not retain message content beyond a short window. + - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + + **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. """ @cached_property @@ -357,6 +641,104 @@ def with_streaming_response(self) -> AsyncMessagesResourceWithStreamingResponse: """ return AsyncMessagesResourceWithStreamingResponse(self) + async def create( + self, + *, + message: MessageContentParam, + to: SequenceNotStr[str], + continuation_message: message_create_params.ContinuationMessage | Omit = omit, + idempotency_key: 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, + ) -> MessageCreateResponse: + """ + Send a message to one or more recipients **without supplying a `from` number**. + Linq resolves both the sending line and the target chat for you, then returns + exactly which line was used, which chat the message landed in, whether a new + chat was created, and every resulting message id. + + This fuses "create chat" and "send message" behind a single message-centric + resource. Provide only the recipients (`to`) and the `message`; the platform + decides the rest. + + ## How the from-number and chat are chosen + + - **Reuse** — if a chat with exactly these recipients already exists and the + line it lives on is healthy, the message is sent into that chat on its + existing line (`from_selection.reason = reused_active_chat`). + - **New** — if no such chat exists, a new chat is created on the best available + line (`from_selection.reason = new_best_number`). + - **Failover** — if a matching chat exists but its line has been flagged, a + **new** chat is created on a fresh best line and the flagged chat is abandoned + (`from_selection.reason = failover_flagged`, `previous_chat_id` set). If you + supply `continuation_message`, that text is sent as the single message INSTEAD + of `message` (useful as a fresh-number-appropriate opener). Exactly one + message is sent either way. + + Recipients (`to`) are an order-independent set: a single handle is a direct + chat, multiple handles a group chat. + + ## Differences from POST /v3/chats + + - The first message **may contain a link** (including for a newly created chat). + Note: sending a link as the very first message on a freshly selected line can + elevate that line's flagging risk — it is allowed, not recommended. + - Voice memos are **not** supported here. To send an iMessage voice-memo bubble, + use `POST /v3/chats/{chatId}/voicememo` with a known chat id. + + ## Service preference, effects, decorations + + Set `message.preferred_service` (`iMessage` | `RCS` | `SMS`), `message.effect`, + and per-part `text_decorations` exactly as on the other send endpoints. + + Always responds `202 Accepted` — chat creation is incidental to the send. + + Args: + message: Message content container. Groups all message-related fields together, + separating the "what" (message content) from the "where" (routing fields like + from/to). + + to: Recipient handles (E.164 phone numbers or email addresses). One handle is a + direct chat; multiple handles a group chat. Order-independent — the set + identifies the chat. + + continuation_message: Text-only fallback that **replaces** `message` ONLY on the failover branch — + when a chat with these recipients already existed but its line was flagged, so a + new chat is created on a fresh line. On that branch this text is sent as the + single message instead of `message` (the recipient is on a new number, so you + typically want a fresh-number-appropriate opener rather than the original + content). Ignored otherwise (a healthy reuse, or genuine first contact). Carries + no parts, media, or effects — exactly one message is ever sent. + + 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 = {**strip_not_given({"Idempotency-Key": idempotency_key}), **(extra_headers or {})} + return await self._post( + "/v3/messages", + body=await async_maybe_transform( + { + "message": message, + "to": to, + "continuation_message": continuation_message, + }, + message_create_params.MessageCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageCreateResponse, + ) + async def retrieve( self, message_id: str, @@ -607,11 +989,111 @@ def list_messages_thread( model=Message, ) + async def update_app_card( + self, + message_id: str, + *, + layout: message_update_app_card_params.Layout, + fallback_text: str | Omit = omit, + interactive: bool | Omit = omit, + url: 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, + ) -> MessageUpdateAppCardResponse: + """ + Replaces a previously delivered `imessage_app` card on the recipient's screen + with new content, instead of posting a new bubble (like a game move redrawing + the board). + + The update is delivered as a **new message** with its own id and delivery + lifecycle (`message.sent` / `message.delivered` / `message.failed` webhooks fire + for the new id). To update the card again, reference the message id returned by + this call. + + Constraints: + + - The referenced message must be an `imessage_app` card sent by you (`400` + otherwise — inbound cards cannot be updated). + - The referenced card must already be delivered (`409` otherwise — retry after + the `message.delivered` webhook for it). + - The app identity (`team_id`, `bundle_id`, name) is inherited from the original + card and cannot change; only `url`, `fallback_text`, and `layout` are + replaced. + - iMessage-only, like all app cards. + - Concurrent updates against the same card are not serialized server-side; the + last one delivered wins on the recipient's screen. Serialize updates by always + referencing the message id returned by the previous call. + + Args: + layout: Visible layout of the card. At least one of `caption`, `subcaption`, + `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise + the card renders as an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders + on the recipient's card whether or not they have your app installed. The small + icon beside the caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title + bold, subtitle beneath it). They only appear when `image_url` is set — without + an image there is nothing to overlay — so setting either without `image_url` is + rejected. + + fallback_text: Text shown on surfaces that cannot render the card (notifications, lock screen). + Defaults to the caption when omitted. + + interactive: Whether the updated card renders as your app's interactive balloon for + recipients who have your iMessage app installed. `true` (default) lets your + installed extension draw its live view; `false` always shows the static `layout` + card. Recipients without your app always see the static card regardless of this + flag. + + Defaults to `true` when omitted — it is **not** inherited from the original + card. To keep a card static across updates, re-send `interactive: false` on each + update. + + url: URL the recipient's app opens when they tap the updated card. + + 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 + """ + if not message_id: + raise ValueError(f"Expected a non-empty value for `message_id` but received {message_id!r}") + return await self._post( + path_template("/v3/messages/{message_id}/update", message_id=message_id), + body=await async_maybe_transform( + { + "layout": layout, + "fallback_text": fallback_text, + "interactive": interactive, + "url": url, + }, + message_update_app_card_params.MessageUpdateAppCardParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MessageUpdateAppCardResponse, + ) + class MessagesResourceWithRawResponse: def __init__(self, messages: MessagesResource) -> None: self._messages = messages + self.create = to_raw_response_wrapper( + messages.create, + ) self.retrieve = to_raw_response_wrapper( messages.retrieve, ) @@ -627,12 +1109,18 @@ def __init__(self, messages: MessagesResource) -> None: self.list_messages_thread = to_raw_response_wrapper( messages.list_messages_thread, ) + self.update_app_card = to_raw_response_wrapper( + messages.update_app_card, + ) class AsyncMessagesResourceWithRawResponse: def __init__(self, messages: AsyncMessagesResource) -> None: self._messages = messages + self.create = async_to_raw_response_wrapper( + messages.create, + ) self.retrieve = async_to_raw_response_wrapper( messages.retrieve, ) @@ -648,12 +1136,18 @@ def __init__(self, messages: AsyncMessagesResource) -> None: self.list_messages_thread = async_to_raw_response_wrapper( messages.list_messages_thread, ) + self.update_app_card = async_to_raw_response_wrapper( + messages.update_app_card, + ) class MessagesResourceWithStreamingResponse: def __init__(self, messages: MessagesResource) -> None: self._messages = messages + self.create = to_streamed_response_wrapper( + messages.create, + ) self.retrieve = to_streamed_response_wrapper( messages.retrieve, ) @@ -669,12 +1163,18 @@ def __init__(self, messages: MessagesResource) -> None: self.list_messages_thread = to_streamed_response_wrapper( messages.list_messages_thread, ) + self.update_app_card = to_streamed_response_wrapper( + messages.update_app_card, + ) class AsyncMessagesResourceWithStreamingResponse: def __init__(self, messages: AsyncMessagesResource) -> None: self._messages = messages + self.create = async_to_streamed_response_wrapper( + messages.create, + ) self.retrieve = async_to_streamed_response_wrapper( messages.retrieve, ) @@ -690,3 +1190,6 @@ def __init__(self, messages: AsyncMessagesResource) -> None: self.list_messages_thread = async_to_streamed_response_wrapper( messages.list_messages_thread, ) + self.update_app_card = async_to_streamed_response_wrapper( + messages.update_app_card, + ) diff --git a/src/linq/resources/phone_numbers.py b/src/linq/resources/phone_numbers.py index 1b4e2f7..007c131 100644 --- a/src/linq/resources/phone_numbers.py +++ b/src/linq/resources/phone_numbers.py @@ -2,9 +2,13 @@ from __future__ import annotations +from typing import Optional + import httpx +from ..types import phone_number_update_params from .._types import Body, Query, Headers, NotGiven, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -15,6 +19,7 @@ ) from .._base_client import make_request_options from ..types.phone_number_list_response import PhoneNumberListResponse +from ..types.phone_number_update_response import PhoneNumberUpdateResponse __all__ = ["PhoneNumbersResource", "AsyncPhoneNumbersResource"] @@ -48,6 +53,49 @@ def with_streaming_response(self) -> PhoneNumbersResourceWithStreamingResponse: """ return PhoneNumbersResourceWithStreamingResponse(self) + def update( + self, + phone_number_id: str, + *, + forwarding_number: Optional[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, + ) -> PhoneNumberUpdateResponse: + """Updates the forwarding number for a phone number. + + The forwarding number is where + inbound calls will be forwarded to. + + Pass an empty string to clear the forwarding number. + + Args: + forwarding_number: The forwarding number in E.164 format. Set to null or empty string to clear. + + 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 + """ + if not phone_number_id: + raise ValueError(f"Expected a non-empty value for `phone_number_id` but received {phone_number_id!r}") + return self._put( + path_template("/v3/phone_numbers/{phone_number_id}", phone_number_id=phone_number_id), + body=maybe_transform( + {"forwarding_number": forwarding_number}, phone_number_update_params.PhoneNumberUpdateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PhoneNumberUpdateResponse, + ) + def list( self, *, @@ -102,6 +150,49 @@ def with_streaming_response(self) -> AsyncPhoneNumbersResourceWithStreamingRespo """ return AsyncPhoneNumbersResourceWithStreamingResponse(self) + async def update( + self, + phone_number_id: str, + *, + forwarding_number: Optional[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, + ) -> PhoneNumberUpdateResponse: + """Updates the forwarding number for a phone number. + + The forwarding number is where + inbound calls will be forwarded to. + + Pass an empty string to clear the forwarding number. + + Args: + forwarding_number: The forwarding number in E.164 format. Set to null or empty string to clear. + + 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 + """ + if not phone_number_id: + raise ValueError(f"Expected a non-empty value for `phone_number_id` but received {phone_number_id!r}") + return await self._put( + path_template("/v3/phone_numbers/{phone_number_id}", phone_number_id=phone_number_id), + body=await async_maybe_transform( + {"forwarding_number": forwarding_number}, phone_number_update_params.PhoneNumberUpdateParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=PhoneNumberUpdateResponse, + ) + async def list( self, *, @@ -131,6 +222,9 @@ class PhoneNumbersResourceWithRawResponse: def __init__(self, phone_numbers: PhoneNumbersResource) -> None: self._phone_numbers = phone_numbers + self.update = to_raw_response_wrapper( + phone_numbers.update, + ) self.list = to_raw_response_wrapper( phone_numbers.list, ) @@ -140,6 +234,9 @@ class AsyncPhoneNumbersResourceWithRawResponse: def __init__(self, phone_numbers: AsyncPhoneNumbersResource) -> None: self._phone_numbers = phone_numbers + self.update = async_to_raw_response_wrapper( + phone_numbers.update, + ) self.list = async_to_raw_response_wrapper( phone_numbers.list, ) @@ -149,6 +246,9 @@ class PhoneNumbersResourceWithStreamingResponse: def __init__(self, phone_numbers: PhoneNumbersResource) -> None: self._phone_numbers = phone_numbers + self.update = to_streamed_response_wrapper( + phone_numbers.update, + ) self.list = to_streamed_response_wrapper( phone_numbers.list, ) @@ -158,6 +258,9 @@ class AsyncPhoneNumbersResourceWithStreamingResponse: def __init__(self, phone_numbers: AsyncPhoneNumbersResource) -> None: self._phone_numbers = phone_numbers + self.update = async_to_streamed_response_wrapper( + phone_numbers.update, + ) self.list = async_to_streamed_response_wrapper( phone_numbers.list, ) diff --git a/src/linq/types/__init__.py b/src/linq/types/__init__.py index fae64a1..f8e252f 100644 --- a/src/linq/types/__init__.py +++ b/src/linq/types/__init__.py @@ -33,10 +33,12 @@ from .webhook_subscription import WebhookSubscription as WebhookSubscription from .handle_check_response import HandleCheckResponse as HandleCheckResponse from .message_content_param import MessageContentParam as MessageContentParam +from .message_create_params import MessageCreateParams as MessageCreateParams from .message_update_params import MessageUpdateParams as MessageUpdateParams from .chat_list_chats_params import ChatListChatsParams as ChatListChatsParams from .schemas_message_effect import SchemasMessageEffect as SchemasMessageEffect from .supported_content_type import SupportedContentType as SupportedContentType +from .message_create_response import MessageCreateResponse as MessageCreateResponse from .attachment_create_params import AttachmentCreateParams as AttachmentCreateParams from .chat_leave_chat_response import ChatLeaveChatResponse as ChatLeaveChatResponse from .phonenumber_list_response import PhonenumberListResponse as PhonenumberListResponse @@ -48,6 +50,7 @@ from .message_read_webhook_event import MessageReadWebhookEvent as MessageReadWebhookEvent from .message_sent_webhook_event import MessageSentWebhookEvent as MessageSentWebhookEvent 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 .capability_check_RCS_params import CapabilityCheckRCSParams as CapabilityCheckRCSParams from .message_add_reaction_params import MessageAddReactionParams as MessageAddReactionParams @@ -58,15 +61,20 @@ from .contact_card_retrieve_params import ContactCardRetrieveParams as ContactCardRetrieveParams from .message_edited_webhook_event import MessageEditedWebhookEvent as MessageEditedWebhookEvent from .message_failed_webhook_event import MessageFailedWebhookEvent as MessageFailedWebhookEvent +from .phone_number_update_response import PhoneNumberUpdateResponse as PhoneNumberUpdateResponse from .reaction_added_webhook_event import ReactionAddedWebhookEvent as ReactionAddedWebhookEvent from .message_add_reaction_response import MessageAddReactionResponse as MessageAddReactionResponse from .contact_card_retrieve_response import ContactCardRetrieveResponse as ContactCardRetrieveResponse from .message_received_webhook_event import MessageReceivedWebhookEvent as MessageReceivedWebhookEvent +from .message_update_app_card_params import MessageUpdateAppCardParams as MessageUpdateAppCardParams from .reaction_removed_webhook_event import ReactionRemovedWebhookEvent as ReactionRemovedWebhookEvent from .message_delivered_webhook_event import MessageDeliveredWebhookEvent as MessageDeliveredWebhookEvent from .participant_added_webhook_event import ParticipantAddedWebhookEvent as ParticipantAddedWebhookEvent +from .available_number_retrieve_params import AvailableNumberRetrieveParams as AvailableNumberRetrieveParams +from .message_update_app_card_response import MessageUpdateAppCardResponse as MessageUpdateAppCardResponse from .capability_check_i_message_params import CapabilityCheckIMessageParams as CapabilityCheckIMessageParams from .participant_removed_webhook_event import ParticipantRemovedWebhookEvent as ParticipantRemovedWebhookEvent +from .available_number_retrieve_response import AvailableNumberRetrieveResponse as AvailableNumberRetrieveResponse from .webhook_subscription_create_params import WebhookSubscriptionCreateParams as WebhookSubscriptionCreateParams from .webhook_subscription_list_response import WebhookSubscriptionListResponse as WebhookSubscriptionListResponse from .webhook_subscription_update_params import WebhookSubscriptionUpdateParams as WebhookSubscriptionUpdateParams diff --git a/src/linq/types/available_number_retrieve_params.py b/src/linq/types/available_number_retrieve_params.py new file mode 100644 index 0000000..a0fddcd --- /dev/null +++ b/src/linq/types/available_number_retrieve_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 TypedDict + +from .._types import SequenceNotStr + +__all__ = ["AvailableNumberRetrieveParams"] + + +class AvailableNumberRetrieveParams(TypedDict, total=False): + to: SequenceNotStr[str] + """Recipient handles (E.164 or email) the message is destined for. + + When provided, an existing chat with these recipients makes the choice sticky. + Repeat the parameter for multiple recipients. + """ diff --git a/src/linq/types/available_number_retrieve_response.py b/src/linq/types/available_number_retrieve_response.py new file mode 100644 index 0000000..0819b94 --- /dev/null +++ b/src/linq/types/available_number_retrieve_response.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["AvailableNumberRetrieveResponse"] + + +class AvailableNumberRetrieveResponse(BaseModel): + """The line smart number assignment selected, plus a shareable vCard.""" + + phone_number: str + """The selected sending line in E.164 format.""" + + vcf_url: str + """Time-limited link to a vCard (`.vcf`) for the selected line. + + The card carries the line's contact details with the selected number as the + primary `TEL` and the partner's other healthy lines as backups. The link + expires; re-call this endpoint to mint a fresh one. + """ diff --git a/src/linq/types/chats/sent_message.py b/src/linq/types/chats/sent_message.py index 4ea7417..c931267 100644 --- a/src/linq/types/chats/sent_message.py +++ b/src/linq/types/chats/sent_message.py @@ -47,14 +47,44 @@ class PartIMessageAppPartResponseLayout(BaseModel): """Visible layout of the card. At least one of - `caption`, `subcaption`, `trailing_caption`, or `trailing_subcaption` must be set, otherwise - the card renders as an empty bubble. Any image on the card is drawn by the recipient's - installed app extension; it cannot be supplied here. + `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be + set, otherwise the card renders as an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders on the + recipient's card whether or not they have your app installed. The small icon beside the + caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle + beneath it). They only appear when `image_url` is set — without an image there is nothing to + overlay — so setting either without `image_url` is rejected. """ caption: Optional[str] = None """Primary label, top-left and bold.""" + image_subtitle: Optional[str] = None + """Text shown below `image_title`, overlaid on the card image. + + Requires `image_url`. + """ + + image_title: Optional[str] = None + """Bold text overlaid on the card image. + + Requires `image_url` (rejected without it). + """ + + image_url: Optional[str] = None + """ + URL of an image (JPEG, PNG, HEIF, or WebP) to display as the card's preview + image; an unreachable or non-image URL returns a validation error. Renders for + all recipients regardless of whether they have the app. Note - requires a + trusted chat w/ inbound activity. In responses, this is the re-hosted + `cdn.linqapp.com` copy of the image you supplied, not your original URL. + """ + subcaption: Optional[str] = None """Secondary label, below `caption` on the left.""" @@ -74,10 +104,20 @@ class PartIMessageAppPartResponse(BaseModel): layout: PartIMessageAppPartResponseLayout """Visible layout of the card. - At least one of `caption`, `subcaption`, `trailing_caption`, or - `trailing_subcaption` must be set, otherwise the card renders as an empty - bubble. Any image on the card is drawn by the recipient's installed app - extension; it cannot be supplied here. + At least one of `caption`, `subcaption`, `trailing_caption`, + `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as + an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders + on the recipient's card whether or not they have your app installed. The small + icon beside the caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title + bold, subtitle beneath it). They only appear when `image_url` is set — without + an image there is nothing to overlay — so setting either without `image_url` is + rejected. """ reactions: Optional[List[Reaction]] = None diff --git a/src/linq/types/contact_card_create_params.py b/src/linq/types/contact_card_create_params.py index ed69c3d..487ede1 100644 --- a/src/linq/types/contact_card_create_params.py +++ b/src/linq/types/contact_card_create_params.py @@ -15,10 +15,7 @@ class ContactCardCreateParams(TypedDict, total=False): """E.164 phone number to associate the contact card with""" image_url: str - """URL of the profile image to rehost on the CDN. - - Only re-uploaded when a new value is provided. - """ + """Profile image URL for the contact card.""" last_name: str """Last name for the contact card. Optional.""" diff --git a/src/linq/types/message.py b/src/linq/types/message.py index 256968b..ed80efc 100644 --- a/src/linq/types/message.py +++ b/src/linq/types/message.py @@ -49,14 +49,44 @@ class PartIMessageAppPartResponseLayout(BaseModel): """Visible layout of the card. At least one of - `caption`, `subcaption`, `trailing_caption`, or `trailing_subcaption` must be set, otherwise - the card renders as an empty bubble. Any image on the card is drawn by the recipient's - installed app extension; it cannot be supplied here. + `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be + set, otherwise the card renders as an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders on the + recipient's card whether or not they have your app installed. The small icon beside the + caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle + beneath it). They only appear when `image_url` is set — without an image there is nothing to + overlay — so setting either without `image_url` is rejected. """ caption: Optional[str] = None """Primary label, top-left and bold.""" + image_subtitle: Optional[str] = None + """Text shown below `image_title`, overlaid on the card image. + + Requires `image_url`. + """ + + image_title: Optional[str] = None + """Bold text overlaid on the card image. + + Requires `image_url` (rejected without it). + """ + + image_url: Optional[str] = None + """ + URL of an image (JPEG, PNG, HEIF, or WebP) to display as the card's preview + image; an unreachable or non-image URL returns a validation error. Renders for + all recipients regardless of whether they have the app. Note - requires a + trusted chat w/ inbound activity. In responses, this is the re-hosted + `cdn.linqapp.com` copy of the image you supplied, not your original URL. + """ + subcaption: Optional[str] = None """Secondary label, below `caption` on the left.""" @@ -76,10 +106,20 @@ class PartIMessageAppPartResponse(BaseModel): layout: PartIMessageAppPartResponseLayout """Visible layout of the card. - At least one of `caption`, `subcaption`, `trailing_caption`, or - `trailing_subcaption` must be set, otherwise the card renders as an empty - bubble. Any image on the card is drawn by the recipient's installed app - extension; it cannot be supplied here. + At least one of `caption`, `subcaption`, `trailing_caption`, + `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as + an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders + on the recipient's card whether or not they have your app installed. The small + icon beside the caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title + bold, subtitle beneath it). They only appear when `image_url` is set — without + an image there is nothing to overlay — so setting either without `image_url` is + rejected. """ reactions: Optional[List[Reaction]] = None diff --git a/src/linq/types/message_content_param.py b/src/linq/types/message_content_param.py index f3eccb4..db5405f 100644 --- a/src/linq/types/message_content_param.py +++ b/src/linq/types/message_content_param.py @@ -39,14 +39,44 @@ class PartIMessageAppPartLayout(TypedDict, total=False): """Visible layout of the card. At least one of - `caption`, `subcaption`, `trailing_caption`, or `trailing_subcaption` must be set, otherwise - the card renders as an empty bubble. Any image on the card is drawn by the recipient's - installed app extension; it cannot be supplied here. + `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be + set, otherwise the card renders as an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders on the + recipient's card whether or not they have your app installed. The small icon beside the + caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle + beneath it). They only appear when `image_url` is set — without an image there is nothing to + overlay — so setting either without `image_url` is rejected. """ caption: str """Primary label, top-left and bold.""" + image_subtitle: str + """Text shown below `image_title`, overlaid on the card image. + + Requires `image_url`. + """ + + image_title: str + """Bold text overlaid on the card image. + + Requires `image_url` (rejected without it). + """ + + image_url: str + """ + URL of an image (JPEG, PNG, HEIF, or WebP) to display as the card's preview + image; an unreachable or non-image URL returns a validation error. Renders for + all recipients regardless of whether they have the app. Note - requires a + trusted chat w/ inbound activity. In responses, this is the re-hosted + `cdn.linqapp.com` copy of the image you supplied, not your original URL. + """ + subcaption: str """Secondary label, below `caption` on the left.""" @@ -72,27 +102,49 @@ class PartIMessageAppPart(TypedDict, total=False): layout: Required[PartIMessageAppPartLayout] """Visible layout of the card. - At least one of `caption`, `subcaption`, `trailing_caption`, or - `trailing_subcaption` must be set, otherwise the card renders as an empty - bubble. Any image on the card is drawn by the recipient's installed app - extension; it cannot be supplied here. + At least one of `caption`, `subcaption`, `trailing_caption`, + `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as + an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders + on the recipient's card whether or not they have your app installed. The small + icon beside the caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title + bold, subtitle beneath it). They only appear when `image_url` is set — without + an image there is nothing to overlay — so setting either without `image_url` is + rejected. """ type: Required[Literal["imessage_app"]] """Indicates this is an iMessage app card part.""" - url: Required[str] - """ - Absolute HTTPS URL delivered to the recipient's installed iMessage app when they - tap the card. Opaque to Messages. - """ - fallback_text: str """Text shown on surfaces that cannot render the card (notifications, lock screen). Defaults to the caption when omitted. """ + interactive: bool + """ + Whether the card renders as your app's interactive balloon for recipients who + have your iMessage app installed. `true` (default) lets your installed extension + draw its live, interactive view for those recipients; everyone else sees the + static card built from `layout`. `false` always shows the static `layout` card, + even to recipients who have the app installed. Recipients without your app + always see the static card regardless of this flag. + """ + + url: str + """URL the recipient's app opens when they tap the card. + + Either an absolute `https://` URL (capped at 2048 characters) or a `data:` URL + carrying inline app state, e.g. a game's encoded state (capped at 16384 + characters). + """ + Part: TypeAlias = Union[TextPartParam, MediaPartParam, LinkPartParam, PartIMessageAppPart] diff --git a/src/linq/types/message_create_params.py b/src/linq/types/message_create_params.py new file mode 100644 index 0000000..f4ff991 --- /dev/null +++ b/src/linq/types/message_create_params.py @@ -0,0 +1,55 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, Annotated, TypedDict + +from .._types import SequenceNotStr +from .._utils import PropertyInfo +from .message_content_param import MessageContentParam + +__all__ = ["MessageCreateParams", "ContinuationMessage"] + + +class MessageCreateParams(TypedDict, total=False): + message: Required[MessageContentParam] + """Message content container. + + Groups all message-related fields together, separating the "what" (message + content) from the "where" (routing fields like from/to). + """ + + to: Required[SequenceNotStr[str]] + """Recipient handles (E.164 phone numbers or email addresses). + + One handle is a direct chat; multiple handles a group chat. Order-independent — + the set identifies the chat. + """ + + continuation_message: ContinuationMessage + """ + Text-only fallback that **replaces** `message` ONLY on the failover branch — + when a chat with these recipients already existed but its line was flagged, so a + new chat is created on a fresh line. On that branch this text is sent as the + single message instead of `message` (the recipient is on a new number, so you + typically want a fresh-number-appropriate opener rather than the original + content). Ignored otherwise (a healthy reuse, or genuine first contact). Carries + no parts, media, or effects — exactly one message is ever sent. + """ + + idempotency_key: Annotated[str, PropertyInfo(alias="Idempotency-Key")] + + +class ContinuationMessage(TypedDict, total=False): + """ + Text-only fallback that **replaces** `message` ONLY on the failover branch — + when a chat with these recipients already existed but its line was flagged, + so a new chat is created on a fresh line. On that branch this text is sent as + the single message instead of `message` (the recipient is on a new number, so + you typically want a fresh-number-appropriate opener rather than the original + content). Ignored otherwise (a healthy reuse, or genuine first contact). + Carries no parts, media, or effects — exactly one message is ever sent. + """ + + text: Required[str] + """The replacement message text, sent as the single message on failover.""" diff --git a/src/linq/types/message_create_response.py b/src/linq/types/message_create_response.py new file mode 100644 index 0000000..f4d8d2d --- /dev/null +++ b/src/linq/types/message_create_response.py @@ -0,0 +1,67 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from .._models import BaseModel +from .chats.sent_message import SentMessage +from .shared.chat_handle import ChatHandle +from .shared.service_type import ServiceType + +__all__ = ["MessageCreateResponse", "FromSelection"] + + +class FromSelection(BaseModel): + """Why this line/chat was chosen.""" + + reason: Literal["reused_active_chat", "new_best_number", "failover_flagged"] + """ + - `reused_active_chat` — reused an existing chat on its healthy line + - `new_best_number` — created a new chat on the best available line + - `failover_flagged` — prior chat's line was flagged; created a new chat on a + fresh line + """ + + reused_existing_chat: bool + """True only when an existing chat was reused.""" + + +class MessageCreateResponse(BaseModel): + """Result of an auto-from send. + + Self-describing: which line was used, which + chat the message landed in, whether a new chat was created, and the + resulting message id(s). + """ + + chat_id: str + """The resolved chat (reused or newly created) the message landed in.""" + + created_new_chat: bool + """True when a new chat was created (new or failover), false on reuse.""" + + from_: str = FieldInfo(alias="from") + """The line (E.164) the message was actually sent from.""" + + from_selection: FromSelection + """Why this line/chat was chosen.""" + + handles: List[ChatHandle] + """Participants of the resolved chat.""" + + is_group: bool + """Whether the resolved chat is a group chat.""" + + message: SentMessage + """A message that was sent (used in CreateChat and SendMessage responses)""" + + service: ServiceType + """Messaging service type""" + + previous_chat_id: Optional[str] = None + """ + Set ONLY on `failover_flagged`: the abandoned flagged chat that was NOT sent + into. Null otherwise. + """ diff --git a/src/linq/types/message_event_v2.py b/src/linq/types/message_event_v2.py index 7cf6c6f..779a882 100644 --- a/src/linq/types/message_event_v2.py +++ b/src/linq/types/message_event_v2.py @@ -132,7 +132,7 @@ class PartSchemasIMessageAppPartResponse(BaseModel): """Indicates this is an iMessage app card part.""" url: str - """The URL delivered to the iMessage app on tap.""" + """The URL the recipient's app opens when the user taps the card.""" fallback_text: Optional[str] = None """Fallback text for surfaces that cannot render the card.""" diff --git a/src/linq/types/message_update_app_card_params.py b/src/linq/types/message_update_app_card_params.py new file mode 100644 index 0000000..d01337d --- /dev/null +++ b/src/linq/types/message_update_app_card_params.py @@ -0,0 +1,102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["MessageUpdateAppCardParams", "Layout"] + + +class MessageUpdateAppCardParams(TypedDict, total=False): + layout: Required[Layout] + """Visible layout of the card. + + At least one of `caption`, `subcaption`, `trailing_caption`, + `trailing_subcaption`, or `image_url` must be set, otherwise the card renders as + an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders + on the recipient's card whether or not they have your app installed. The small + icon beside the caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title + bold, subtitle beneath it). They only appear when `image_url` is set — without + an image there is nothing to overlay — so setting either without `image_url` is + rejected. + """ + + fallback_text: str + """Text shown on surfaces that cannot render the card (notifications, lock screen). + + Defaults to the caption when omitted. + """ + + interactive: bool + """ + Whether the updated card renders as your app's interactive balloon for + recipients who have your iMessage app installed. `true` (default) lets your + installed extension draw its live view; `false` always shows the static `layout` + card. Recipients without your app always see the static card regardless of this + flag. + + Defaults to `true` when omitted — it is **not** inherited from the original + card. To keep a card static across updates, re-send `interactive: false` on each + update. + """ + + url: str + """URL the recipient's app opens when they tap the updated card.""" + + +class Layout(TypedDict, total=False): + """Visible layout of the card. + + At least one of + `caption`, `subcaption`, `trailing_caption`, `trailing_subcaption`, or `image_url` must be + set, otherwise the card renders as an empty bubble. + + `image_url` displays a preview image at the top of the card. The image renders on the + recipient's card whether or not they have your app installed. The small icon beside the + caption is the app's own icon and is not settable here. + + `* Note - requires a trusted chat w/ inbound activity` + + `image_title` and `image_subtitle` render as text overlaid on the image (title bold, subtitle + beneath it). They only appear when `image_url` is set — without an image there is nothing to + overlay — so setting either without `image_url` is rejected. + """ + + caption: str + """Primary label, top-left and bold.""" + + image_subtitle: str + """Text shown below `image_title`, overlaid on the card image. + + Requires `image_url`. + """ + + image_title: str + """Bold text overlaid on the card image. + + Requires `image_url` (rejected without it). + """ + + image_url: str + """ + URL of an image (JPEG, PNG, HEIF, or WebP) to display as the card's preview + image; an unreachable or non-image URL returns a validation error. Renders for + all recipients regardless of whether they have the app. Note - requires a + trusted chat w/ inbound activity. In responses, this is the re-hosted + `cdn.linqapp.com` copy of the image you supplied, not your original URL. + """ + + subcaption: str + """Secondary label, below `caption` on the left.""" + + trailing_caption: str + """Label shown top-right.""" + + trailing_subcaption: str + """Label shown below `trailing_caption`, on the right.""" diff --git a/src/linq/types/message_update_app_card_response.py b/src/linq/types/message_update_app_card_response.py new file mode 100644 index 0000000..bac3ea2 --- /dev/null +++ b/src/linq/types/message_update_app_card_response.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel +from .chats.sent_message import SentMessage + +__all__ = ["MessageUpdateAppCardResponse"] + + +class MessageUpdateAppCardResponse(BaseModel): + """Response for sending a message to a chat""" + + chat_id: str + """Unique identifier of the chat this message was sent to""" + + message: SentMessage + """A message that was sent (used in CreateChat and SendMessage responses)""" diff --git a/src/linq/types/phone_number_list_response.py b/src/linq/types/phone_number_list_response.py index 58822cc..49eb74d 100644 --- a/src/linq/types/phone_number_list_response.py +++ b/src/linq/types/phone_number_list_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import List, Optional from typing_extensions import Literal from .._models import BaseModel @@ -15,20 +15,22 @@ class PhoneNumberHealthStatus(BaseModel): Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. - See the [Phone Health guide](/guides/phone-numbers/phone-health) for what each status means and how to react. + See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for what each status means and how to react. """ doc_url: str - """Deep-link to the relevant section of the Phone Health guide for this status.""" + """ + Deep-link to the relevant section of the Phone Reputation guide for this status. + """ status: Literal["HEALTHY", "AT_RISK", "CRITICAL"] - """Current reputation of this phone line as assessed by risk-service. + """Current reputation of this phone line. - - `HEALTHY` — No elevated risk detected. - - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or - reviewing messaging patterns. - - `CRITICAL` — High risk; further sending may result in line flagging or - restriction. + - `HEALTHY` — The line is in good standing. Send normally. + - `AT_RISK` — The line's overall engagement is trending down. Slow the line's + send pace and review your messaging patterns. + - `CRITICAL` — Strong signals that messages from this line aren't landing well. + Pause outbound on the line until it recovers. Defaults to `HEALTHY` for lines that have not yet been scored. """ @@ -41,20 +43,22 @@ class PhoneNumberReputation(BaseModel): Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. - See the [Phone Health guide](/guides/phone-numbers/phone-health) for what each status means and how to react. + See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for what each status means and how to react. """ doc_url: str - """Deep-link to the relevant section of the Phone Health guide for this status.""" + """ + Deep-link to the relevant section of the Phone Reputation guide for this status. + """ status: Literal["HEALTHY", "AT_RISK", "CRITICAL"] - """Current reputation of this phone line as assessed by risk-service. + """Current reputation of this phone line. - - `HEALTHY` — No elevated risk detected. - - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or - reviewing messaging patterns. - - `CRITICAL` — High risk; further sending may result in line flagging or - restriction. + - `HEALTHY` — The line is in good standing. Send normally. + - `AT_RISK` — The line's overall engagement is trending down. Slow the line's + send pace and review your messaging patterns. + - `CRITICAL` — Strong signals that messages from this line aren't landing well. + Pause outbound on the line until it recovers. Defaults to `HEALTHY` for lines that have not yet been scored. """ @@ -73,8 +77,8 @@ class PhoneNumber(BaseModel): Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. - See the [Phone Health guide](/guides/phone-numbers/phone-health) for what each - status means and how to react. + See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for + what each status means and how to react. """ phone_number: str @@ -89,8 +93,14 @@ class PhoneNumber(BaseModel): Unlike chat health, line reputation does not include `opted_out` — opt-out applies to individual recipients, not the whole line. - See the [Phone Health guide](/guides/phone-numbers/phone-health) for what each - status means and how to react. + See the [Phone Reputation guide](/guides/phone-numbers/phone-reputation) for + what each status means and how to react. + """ + + forwarding_number: Optional[str] = None + """The forwarding number associated with this phone number, in E.164 format. + + Null when no forwarding number is configured. """ diff --git a/src/linq/types/phone_number_status_updated_webhook_event.py b/src/linq/types/phone_number_status_updated_webhook_event.py index 0d4f745..13ce18c 100644 --- a/src/linq/types/phone_number_status_updated_webhook_event.py +++ b/src/linq/types/phone_number_status_updated_webhook_event.py @@ -15,13 +15,13 @@ class Data(BaseModel): """When the status change occurred""" new_health_status: Literal["HEALTHY", "AT_RISK", "CRITICAL"] - """Current reputation of this phone line as assessed by risk-service. + """Current reputation of this phone line. - - `HEALTHY` — No elevated risk detected. - - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or - reviewing messaging patterns. - - `CRITICAL` — High risk; further sending may result in line flagging or - restriction. + - `HEALTHY` — The line is in good standing. Send normally. + - `AT_RISK` — The line's overall engagement is trending down. Slow the line's + send pace and review your messaging patterns. + - `CRITICAL` — Strong signals that messages from this line aren't landing well. + Pause outbound on the line until it recovers. Defaults to `HEALTHY` for lines that have not yet been scored. """ @@ -36,13 +36,13 @@ class Data(BaseModel): """Phone number in E.164 format""" previous_health_status: Literal["HEALTHY", "AT_RISK", "CRITICAL"] - """Current reputation of this phone line as assessed by risk-service. + """Current reputation of this phone line. - - `HEALTHY` — No elevated risk detected. - - `AT_RISK` — Elevated risk indicators present; consider reducing send volume or - reviewing messaging patterns. - - `CRITICAL` — High risk; further sending may result in line flagging or - restriction. + - `HEALTHY` — The line is in good standing. Send normally. + - `AT_RISK` — The line's overall engagement is trending down. Slow the line's + send pace and review your messaging patterns. + - `CRITICAL` — Strong signals that messages from this line aren't landing well. + Pause outbound on the line until it recovers. Defaults to `HEALTHY` for lines that have not yet been scored. """ diff --git a/src/linq/types/phone_number_update_params.py b/src/linq/types/phone_number_update_params.py new file mode 100644 index 0000000..8016556 --- /dev/null +++ b/src/linq/types/phone_number_update_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["PhoneNumberUpdateParams"] + + +class PhoneNumberUpdateParams(TypedDict, total=False): + forwarding_number: Required[Optional[str]] + """The forwarding number in E.164 format. Set to null or empty string to clear.""" diff --git a/src/linq/types/phone_number_update_response.py b/src/linq/types/phone_number_update_response.py new file mode 100644 index 0000000..46fffb5 --- /dev/null +++ b/src/linq/types/phone_number_update_response.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from .._models import BaseModel + +__all__ = ["PhoneNumberUpdateResponse"] + + +class PhoneNumberUpdateResponse(BaseModel): + id: str + """Unique identifier for the phone number""" + + forwarding_number: Optional[str] = None + """The forwarding number after the update. Null when cleared.""" + + phone_number: str + """Phone number in E.164 format""" diff --git a/tests/api_resources/test_available_number.py b/tests/api_resources/test_available_number.py new file mode 100644 index 0000000..b2f1062 --- /dev/null +++ b/tests/api_resources/test_available_number.py @@ -0,0 +1,96 @@ +# 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 AvailableNumberRetrieveResponse +from tests.utils import assert_matches_type + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAvailableNumber: + 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_retrieve(self, client: LinqAPIV3) -> None: + available_number = client.available_number.retrieve() + assert_matches_type(AvailableNumberRetrieveResponse, available_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_with_all_params(self, client: LinqAPIV3) -> None: + available_number = client.available_number.retrieve( + to=["string"], + ) + assert_matches_type(AvailableNumberRetrieveResponse, available_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: LinqAPIV3) -> None: + response = client.available_number.with_raw_response.retrieve() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + available_number = response.parse() + assert_matches_type(AvailableNumberRetrieveResponse, available_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: LinqAPIV3) -> None: + with client.available_number.with_streaming_response.retrieve() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + available_number = response.parse() + assert_matches_type(AvailableNumberRetrieveResponse, available_number, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAvailableNumber: + 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_retrieve(self, async_client: AsyncLinqAPIV3) -> None: + available_number = await async_client.available_number.retrieve() + assert_matches_type(AvailableNumberRetrieveResponse, available_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_with_all_params(self, async_client: AsyncLinqAPIV3) -> None: + available_number = await async_client.available_number.retrieve( + to=["string"], + ) + assert_matches_type(AvailableNumberRetrieveResponse, available_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.available_number.with_raw_response.retrieve() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + available_number = await response.parse() + assert_matches_type(AvailableNumberRetrieveResponse, available_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.available_number.with_streaming_response.retrieve() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + available_number = await response.parse() + assert_matches_type(AvailableNumberRetrieveResponse, available_number, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_messages.py b/tests/api_resources/test_messages.py index 7b9d2b9..9ceb69b 100644 --- a/tests/api_resources/test_messages.py +++ b/tests/api_resources/test_messages.py @@ -10,7 +10,9 @@ from linq import LinqAPIV3, AsyncLinqAPIV3 from linq.types import ( Message, + MessageCreateResponse, MessageAddReactionResponse, + MessageUpdateAppCardResponse, ) from tests.utils import assert_matches_type from linq.pagination import SyncListMessagesPagination, AsyncListMessagesPagination @@ -21,6 +23,104 @@ class TestMessages: 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_create(self, client: LinqAPIV3) -> None: + message = client.messages.create( + message={ + "parts": [ + { + "type": "text", + "value": "Hi! Thanks for reaching out — how can we help?", + } + ] + }, + to=["+14155559876"], + ) + assert_matches_type(MessageCreateResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: LinqAPIV3) -> None: + message = client.messages.create( + message={ + "parts": [ + { + "type": "text", + "value": "Hi! Thanks for reaching out — how can we help?", + "text_decorations": [ + { + "range": [0, 5], + "animation": "shake", + "style": "bold", + }, + { + "range": [6, 11], + "animation": "shake", + "style": "bold", + }, + ], + } + ], + "effect": { + "name": "confetti", + "type": "screen", + }, + "idempotency_key": "msg-abc123xyz", + "preferred_service": "iMessage", + "reply_to": { + "message_id": "550e8400-e29b-41d4-a716-446655440000", + "part_index": 0, + }, + }, + to=["+14155559876"], + continuation_message={"text": "Hi, it's Acme Support reaching you from a new number."}, + idempotency_key="send-abc123xyz", + ) + assert_matches_type(MessageCreateResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: LinqAPIV3) -> None: + response = client.messages.with_raw_response.create( + message={ + "parts": [ + { + "type": "text", + "value": "Hi! Thanks for reaching out — how can we help?", + } + ] + }, + to=["+14155559876"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(MessageCreateResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: LinqAPIV3) -> None: + with client.messages.with_streaming_response.create( + message={ + "parts": [ + { + "type": "text", + "value": "Hi! Thanks for reaching out — how can we help?", + } + ] + }, + to=["+14155559876"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(MessageCreateResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_retrieve(self, client: LinqAPIV3) -> None: @@ -276,12 +376,176 @@ def test_path_params_list_messages_thread(self, client: LinqAPIV3) -> None: message_id="", ) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_app_card(self, client: LinqAPIV3) -> None: + message = client.messages.update_app_card( + message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a", + layout={}, + ) + assert_matches_type(MessageUpdateAppCardResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_app_card_with_all_params(self, client: LinqAPIV3) -> None: + message = client.messages.update_app_card( + message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a", + layout={ + "caption": "Score: 2 – 1", + "image_subtitle": "Tonight, 7:30 PM", + "image_title": "Table for 2", + "image_url": "https://cdn.linqapp.com/example/card-preview.jpg", + "subcaption": "You said: hello", + "trailing_caption": "2 min", + "trailing_subcaption": "expires", + }, + fallback_text="Score update", + interactive=True, + url="https://app.example.com/card?game=7f3a&move=2", + ) + assert_matches_type(MessageUpdateAppCardResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update_app_card(self, client: LinqAPIV3) -> None: + response = client.messages.with_raw_response.update_app_card( + message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a", + layout={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = response.parse() + assert_matches_type(MessageUpdateAppCardResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update_app_card(self, client: LinqAPIV3) -> None: + with client.messages.with_streaming_response.update_app_card( + message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a", + layout={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = response.parse() + assert_matches_type(MessageUpdateAppCardResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update_app_card(self, client: LinqAPIV3) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + client.messages.with_raw_response.update_app_card( + message_id="", + layout={}, + ) + class TestAsyncMessages: 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_create(self, async_client: AsyncLinqAPIV3) -> None: + message = await async_client.messages.create( + message={ + "parts": [ + { + "type": "text", + "value": "Hi! Thanks for reaching out — how can we help?", + } + ] + }, + to=["+14155559876"], + ) + assert_matches_type(MessageCreateResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncLinqAPIV3) -> None: + message = await async_client.messages.create( + message={ + "parts": [ + { + "type": "text", + "value": "Hi! Thanks for reaching out — how can we help?", + "text_decorations": [ + { + "range": [0, 5], + "animation": "shake", + "style": "bold", + }, + { + "range": [6, 11], + "animation": "shake", + "style": "bold", + }, + ], + } + ], + "effect": { + "name": "confetti", + "type": "screen", + }, + "idempotency_key": "msg-abc123xyz", + "preferred_service": "iMessage", + "reply_to": { + "message_id": "550e8400-e29b-41d4-a716-446655440000", + "part_index": 0, + }, + }, + to=["+14155559876"], + continuation_message={"text": "Hi, it's Acme Support reaching you from a new number."}, + idempotency_key="send-abc123xyz", + ) + assert_matches_type(MessageCreateResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.messages.with_raw_response.create( + message={ + "parts": [ + { + "type": "text", + "value": "Hi! Thanks for reaching out — how can we help?", + } + ] + }, + to=["+14155559876"], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(MessageCreateResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.messages.with_streaming_response.create( + message={ + "parts": [ + { + "type": "text", + "value": "Hi! Thanks for reaching out — how can we help?", + } + ] + }, + to=["+14155559876"], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(MessageCreateResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_retrieve(self, async_client: AsyncLinqAPIV3) -> None: @@ -536,3 +800,69 @@ async def test_path_params_list_messages_thread(self, async_client: AsyncLinqAPI await async_client.messages.with_raw_response.list_messages_thread( message_id="", ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_app_card(self, async_client: AsyncLinqAPIV3) -> None: + message = await async_client.messages.update_app_card( + message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a", + layout={}, + ) + assert_matches_type(MessageUpdateAppCardResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_app_card_with_all_params(self, async_client: AsyncLinqAPIV3) -> None: + message = await async_client.messages.update_app_card( + message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a", + layout={ + "caption": "Score: 2 – 1", + "image_subtitle": "Tonight, 7:30 PM", + "image_title": "Table for 2", + "image_url": "https://cdn.linqapp.com/example/card-preview.jpg", + "subcaption": "You said: hello", + "trailing_caption": "2 min", + "trailing_subcaption": "expires", + }, + fallback_text="Score update", + interactive=True, + url="https://app.example.com/card?game=7f3a&move=2", + ) + assert_matches_type(MessageUpdateAppCardResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update_app_card(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.messages.with_raw_response.update_app_card( + message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a", + layout={}, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + message = await response.parse() + assert_matches_type(MessageUpdateAppCardResponse, message, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update_app_card(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.messages.with_streaming_response.update_app_card( + message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a", + layout={}, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + message = await response.parse() + assert_matches_type(MessageUpdateAppCardResponse, message, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update_app_card(self, async_client: AsyncLinqAPIV3) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `message_id` but received ''"): + await async_client.messages.with_raw_response.update_app_card( + message_id="", + layout={}, + ) diff --git a/tests/api_resources/test_phone_numbers.py b/tests/api_resources/test_phone_numbers.py index 4cf9890..49c2e92 100644 --- a/tests/api_resources/test_phone_numbers.py +++ b/tests/api_resources/test_phone_numbers.py @@ -8,7 +8,7 @@ import pytest from linq import LinqAPIV3, AsyncLinqAPIV3 -from linq.types import PhoneNumberListResponse +from linq.types import PhoneNumberListResponse, PhoneNumberUpdateResponse from tests.utils import assert_matches_type base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -17,6 +17,52 @@ class TestPhoneNumbers: 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_update(self, client: LinqAPIV3) -> None: + phone_number = client.phone_numbers.update( + phone_number_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + forwarding_number="+12025559999", + ) + assert_matches_type(PhoneNumberUpdateResponse, phone_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: LinqAPIV3) -> None: + response = client.phone_numbers.with_raw_response.update( + phone_number_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + forwarding_number="+12025559999", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + phone_number = response.parse() + assert_matches_type(PhoneNumberUpdateResponse, phone_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: LinqAPIV3) -> None: + with client.phone_numbers.with_streaming_response.update( + phone_number_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + forwarding_number="+12025559999", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + phone_number = response.parse() + assert_matches_type(PhoneNumberUpdateResponse, phone_number, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: LinqAPIV3) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `phone_number_id` but received ''"): + client.phone_numbers.with_raw_response.update( + phone_number_id="", + forwarding_number="+12025559999", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_method_list(self, client: LinqAPIV3) -> None: @@ -51,6 +97,52 @@ class TestAsyncPhoneNumbers: "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_update(self, async_client: AsyncLinqAPIV3) -> None: + phone_number = await async_client.phone_numbers.update( + phone_number_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + forwarding_number="+12025559999", + ) + assert_matches_type(PhoneNumberUpdateResponse, phone_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncLinqAPIV3) -> None: + response = await async_client.phone_numbers.with_raw_response.update( + phone_number_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + forwarding_number="+12025559999", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + phone_number = await response.parse() + assert_matches_type(PhoneNumberUpdateResponse, phone_number, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncLinqAPIV3) -> None: + async with async_client.phone_numbers.with_streaming_response.update( + phone_number_id="182bd5e5-6e1a-4fe4-a799-aa6d9a6ab26e", + forwarding_number="+12025559999", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + phone_number = await response.parse() + assert_matches_type(PhoneNumberUpdateResponse, phone_number, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncLinqAPIV3) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `phone_number_id` but received ''"): + await async_client.phone_numbers.with_raw_response.update( + phone_number_id="", + forwarding_number="+12025559999", + ) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_method_list(self, async_client: AsyncLinqAPIV3) -> None: diff --git a/uv.lock b/uv.lock index b3dcdff..ee08702 100644 --- a/uv.lock +++ b/uv.lock @@ -530,7 +530,7 @@ wheels = [ [[package]] name = "linq-python" -version = "0.14.0" +version = "0.15.0" source = { editable = "." } dependencies = [ { name = "anyio" },