linq-python 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. linq/__init__.py +102 -0
  2. linq/_base_client.py +2149 -0
  3. linq/_client.py +2479 -0
  4. linq/_compat.py +226 -0
  5. linq/_constants.py +14 -0
  6. linq/_exceptions.py +108 -0
  7. linq/_files.py +123 -0
  8. linq/_models.py +878 -0
  9. linq/_qs.py +153 -0
  10. linq/_resource.py +43 -0
  11. linq/_response.py +833 -0
  12. linq/_streaming.py +338 -0
  13. linq/_types.py +271 -0
  14. linq/_utils/__init__.py +65 -0
  15. linq/_utils/_compat.py +45 -0
  16. linq/_utils/_datetime_parse.py +136 -0
  17. linq/_utils/_json.py +35 -0
  18. linq/_utils/_logs.py +25 -0
  19. linq/_utils/_path.py +127 -0
  20. linq/_utils/_proxy.py +65 -0
  21. linq/_utils/_reflection.py +42 -0
  22. linq/_utils/_resources_proxy.py +24 -0
  23. linq/_utils/_streams.py +12 -0
  24. linq/_utils/_sync.py +58 -0
  25. linq/_utils/_transform.py +457 -0
  26. linq/_utils/_typing.py +156 -0
  27. linq/_utils/_utils.py +421 -0
  28. linq/_version.py +4 -0
  29. linq/lib/.keep +4 -0
  30. linq/pagination.py +95 -0
  31. linq/py.typed +0 -0
  32. linq/resources/__init__.py +134 -0
  33. linq/resources/attachments.py +589 -0
  34. linq/resources/capability.py +297 -0
  35. linq/resources/chats/__init__.py +61 -0
  36. linq/resources/chats/chats.py +1492 -0
  37. linq/resources/chats/messages.py +416 -0
  38. linq/resources/chats/participants.py +322 -0
  39. linq/resources/chats/typing.py +299 -0
  40. linq/resources/contact_card.py +472 -0
  41. linq/resources/messages.py +686 -0
  42. linq/resources/phone_numbers.py +163 -0
  43. linq/resources/phonenumbers.py +165 -0
  44. linq/resources/webhook_events.py +319 -0
  45. linq/resources/webhook_subscriptions.py +776 -0
  46. linq/resources/webhooks.py +34 -0
  47. linq/types/__init__.py +90 -0
  48. linq/types/attachment_create_params.py +42 -0
  49. linq/types/attachment_create_response.py +44 -0
  50. linq/types/attachment_retrieve_response.py +55 -0
  51. linq/types/capability_check_RCS_params.py +20 -0
  52. linq/types/capability_check_i_message_params.py +20 -0
  53. linq/types/chat.py +44 -0
  54. linq/types/chat_create_params.py +33 -0
  55. linq/types/chat_create_response.py +44 -0
  56. linq/types/chat_created_webhook_event.py +87 -0
  57. linq/types/chat_group_icon_update_failed_webhook_event.py +65 -0
  58. linq/types/chat_group_icon_updated_webhook_event.py +66 -0
  59. linq/types/chat_group_name_update_failed_webhook_event.py +65 -0
  60. linq/types/chat_group_name_updated_webhook_event.py +66 -0
  61. linq/types/chat_leave_chat_response.py +15 -0
  62. linq/types/chat_list_chats_params.py +36 -0
  63. linq/types/chat_send_voicememo_params.py +23 -0
  64. linq/types/chat_send_voicememo_response.py +79 -0
  65. linq/types/chat_typing_indicator_started_webhook_event.py +52 -0
  66. linq/types/chat_typing_indicator_stopped_webhook_event.py +52 -0
  67. linq/types/chat_update_params.py +15 -0
  68. linq/types/chat_update_response.py +13 -0
  69. linq/types/chats/__init__.py +12 -0
  70. linq/types/chats/message_list_params.py +15 -0
  71. linq/types/chats/message_send_params.py +18 -0
  72. linq/types/chats/message_send_response.py +16 -0
  73. linq/types/chats/participant_add_params.py +12 -0
  74. linq/types/chats/participant_add_response.py +15 -0
  75. linq/types/chats/participant_remove_params.py +12 -0
  76. linq/types/chats/participant_remove_response.py +15 -0
  77. linq/types/chats/sent_message.py +69 -0
  78. linq/types/contact_card_create_params.py +24 -0
  79. linq/types/contact_card_retrieve_params.py +15 -0
  80. linq/types/contact_card_retrieve_response.py +23 -0
  81. linq/types/contact_card_update_params.py +21 -0
  82. linq/types/events_webhook_event.py +50 -0
  83. linq/types/handle_check_response.py +13 -0
  84. linq/types/link_part_param.py +22 -0
  85. linq/types/media_part_param.py +54 -0
  86. linq/types/message.py +87 -0
  87. linq/types/message_add_reaction_params.py +32 -0
  88. linq/types/message_add_reaction_response.py +15 -0
  89. linq/types/message_content_param.py +82 -0
  90. linq/types/message_delivered_webhook_event.py +65 -0
  91. linq/types/message_edited_webhook_event.py +100 -0
  92. linq/types/message_effect.py +23 -0
  93. linq/types/message_effect_param.py +22 -0
  94. linq/types/message_event_v2.py +116 -0
  95. linq/types/message_failed_webhook_event.py +72 -0
  96. linq/types/message_list_messages_thread_params.py +18 -0
  97. linq/types/message_read_webhook_event.py +65 -0
  98. linq/types/message_received_webhook_event.py +65 -0
  99. linq/types/message_sent_webhook_event.py +65 -0
  100. linq/types/message_update_params.py +15 -0
  101. linq/types/participant_added_webhook_event.py +66 -0
  102. linq/types/participant_removed_webhook_event.py +66 -0
  103. linq/types/phone_number_list_response.py +20 -0
  104. linq/types/phone_number_status_updated_webhook_event.py +82 -0
  105. linq/types/phonenumber_list_response.py +39 -0
  106. linq/types/reaction_added_webhook_event.py +46 -0
  107. linq/types/reaction_event_base.py +85 -0
  108. linq/types/reaction_removed_webhook_event.py +46 -0
  109. linq/types/reply_to.py +21 -0
  110. linq/types/reply_to_param.py +21 -0
  111. linq/types/schemas_media_part_response.py +29 -0
  112. linq/types/schemas_message_effect.py +18 -0
  113. linq/types/schemas_text_part_response.py +22 -0
  114. linq/types/set_contact_card.py +24 -0
  115. linq/types/shared/__init__.py +9 -0
  116. linq/types/shared/chat_handle.py +33 -0
  117. linq/types/shared/media_part_response.py +34 -0
  118. linq/types/shared/reaction.py +56 -0
  119. linq/types/shared/reaction_type.py +7 -0
  120. linq/types/shared/service_type.py +7 -0
  121. linq/types/shared/text_decoration.py +23 -0
  122. linq/types/shared/text_part_response.py +26 -0
  123. linq/types/shared_params/__init__.py +5 -0
  124. linq/types/shared_params/reaction_type.py +9 -0
  125. linq/types/shared_params/service_type.py +9 -0
  126. linq/types/shared_params/text_decoration.py +23 -0
  127. linq/types/supported_content_type.py +60 -0
  128. linq/types/text_part_param.py +44 -0
  129. linq/types/webhook_event_list_response.py +17 -0
  130. linq/types/webhook_event_type.py +33 -0
  131. linq/types/webhook_subscription.py +35 -0
  132. linq/types/webhook_subscription_create_params.py +27 -0
  133. linq/types/webhook_subscription_create_response.py +46 -0
  134. linq/types/webhook_subscription_list_response.py +13 -0
  135. linq/types/webhook_subscription_update_params.py +30 -0
  136. linq_python-0.1.0.dist-info/METADATA +572 -0
  137. linq_python-0.1.0.dist-info/RECORD +139 -0
  138. linq_python-0.1.0.dist-info/WHEEL +4 -0
  139. linq_python-0.1.0.dist-info/licenses/LICENSE +201 -0
linq/_client.py ADDED
@@ -0,0 +1,2479 @@
1
+ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import TYPE_CHECKING, Any, Mapping
7
+ from typing_extensions import Self, override
8
+
9
+ import httpx
10
+
11
+ from . import _exceptions
12
+ from ._qs import Querystring
13
+ from ._types import (
14
+ Omit,
15
+ Timeout,
16
+ NotGiven,
17
+ Transport,
18
+ ProxiesTypes,
19
+ RequestOptions,
20
+ not_given,
21
+ )
22
+ from ._utils import is_given, get_async_library
23
+ from ._compat import cached_property
24
+ from ._models import SecurityOptions
25
+ from ._version import __version__
26
+ from ._streaming import Stream as Stream, AsyncStream as AsyncStream
27
+ from ._exceptions import APIStatusError, LinqApiv3Error
28
+ from ._base_client import (
29
+ DEFAULT_MAX_RETRIES,
30
+ SyncAPIClient,
31
+ AsyncAPIClient,
32
+ )
33
+
34
+ if TYPE_CHECKING:
35
+ from .resources import (
36
+ chats,
37
+ messages,
38
+ capability,
39
+ attachments,
40
+ contact_card,
41
+ phonenumbers,
42
+ phone_numbers,
43
+ webhook_events,
44
+ webhook_subscriptions,
45
+ )
46
+ from .resources.messages import MessagesResource, AsyncMessagesResource
47
+ from .resources.webhooks import WebhooksResource, AsyncWebhooksResource
48
+ from .resources.capability import CapabilityResource, AsyncCapabilityResource
49
+ from .resources.attachments import AttachmentsResource, AsyncAttachmentsResource
50
+ from .resources.chats.chats import ChatsResource, AsyncChatsResource
51
+ from .resources.contact_card import ContactCardResource, AsyncContactCardResource
52
+ from .resources.phonenumbers import PhonenumbersResource, AsyncPhonenumbersResource
53
+ from .resources.phone_numbers import PhoneNumbersResource, AsyncPhoneNumbersResource
54
+ from .resources.webhook_events import WebhookEventsResource, AsyncWebhookEventsResource
55
+ from .resources.webhook_subscriptions import WebhookSubscriptionsResource, AsyncWebhookSubscriptionsResource
56
+
57
+ __all__ = [
58
+ "Timeout",
59
+ "Transport",
60
+ "ProxiesTypes",
61
+ "RequestOptions",
62
+ "LinqAPIV3",
63
+ "AsyncLinqAPIV3",
64
+ "Client",
65
+ "AsyncClient",
66
+ ]
67
+
68
+
69
+ class LinqAPIV3(SyncAPIClient):
70
+ # client options
71
+ api_key: str
72
+
73
+ def __init__(
74
+ self,
75
+ *,
76
+ api_key: str | None = None,
77
+ base_url: str | httpx.URL | None = None,
78
+ timeout: float | Timeout | None | NotGiven = not_given,
79
+ max_retries: int = DEFAULT_MAX_RETRIES,
80
+ default_headers: Mapping[str, str] | None = None,
81
+ default_query: Mapping[str, object] | None = None,
82
+ # Configure a custom httpx client.
83
+ # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
84
+ # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details.
85
+ http_client: httpx.Client | None = None,
86
+ # Enable or disable schema validation for data returned by the API.
87
+ # When enabled an error APIResponseValidationError is raised
88
+ # if the API responds with invalid data for the expected schema.
89
+ #
90
+ # This parameter may be removed or changed in the future.
91
+ # If you rely on this feature, please open a GitHub issue
92
+ # outlining your use-case to help us decide if it should be
93
+ # part of our public interface in the future.
94
+ _strict_response_validation: bool = False,
95
+ ) -> None:
96
+ """Construct a new synchronous LinqAPIV3 client instance.
97
+
98
+ This automatically infers the `api_key` argument from the `LINQ_API_V3_API_KEY` environment variable if it is not provided.
99
+ """
100
+ if api_key is None:
101
+ api_key = os.environ.get("LINQ_API_V3_API_KEY")
102
+ if api_key is None:
103
+ raise LinqApiv3Error(
104
+ "The api_key client option must be set either by passing api_key to the client or by setting the LINQ_API_V3_API_KEY environment variable"
105
+ )
106
+ self.api_key = api_key
107
+
108
+ if base_url is None:
109
+ base_url = os.environ.get("LINQ_API_V3_BASE_URL")
110
+ if base_url is None:
111
+ base_url = f"https://api.linqapp.com/api/partner"
112
+
113
+ super().__init__(
114
+ version=__version__,
115
+ base_url=base_url,
116
+ max_retries=max_retries,
117
+ timeout=timeout,
118
+ http_client=http_client,
119
+ custom_headers=default_headers,
120
+ custom_query=default_query,
121
+ _strict_response_validation=_strict_response_validation,
122
+ )
123
+
124
+ @cached_property
125
+ def chats(self) -> ChatsResource:
126
+ from .resources.chats import ChatsResource
127
+
128
+ return ChatsResource(self)
129
+
130
+ @cached_property
131
+ def messages(self) -> MessagesResource:
132
+ """Messages are individual communications within a chat thread.
133
+
134
+ Messages can include text, media attachments, rich link previews, special effects
135
+ (like confetti or fireworks), and reactions. All messages are associated with a
136
+ specific chat and sent from a phone number you own.
137
+
138
+ Messages support delivery status tracking, read receipts, and editing capabilities.
139
+
140
+ ## Rich Link Previews
141
+
142
+ Send a URL as a `link` part to deliver it with a rich preview card showing the
143
+ page's title, description, and image (when available). A `link` part must be the
144
+ **only** part in the message — it cannot be combined with text or media parts.
145
+ To send a URL without a preview card, include it in a `text` part instead.
146
+
147
+ **Limitations:**
148
+ - A `link` part cannot be combined with other parts in the same message.
149
+ - Maximum URL length: 2,048 characters.
150
+ """
151
+ from .resources.messages import MessagesResource
152
+
153
+ return MessagesResource(self)
154
+
155
+ @cached_property
156
+ def attachments(self) -> AttachmentsResource:
157
+ """
158
+ Send files (images, videos, documents, audio) with messages by providing a URL in a media part.
159
+ Pre-uploading via `POST /v3/attachments` is **optional** and only needed for specific optimization scenarios.
160
+
161
+ ## Sending Media via URL (up to 10MB)
162
+
163
+ Provide a publicly accessible HTTPS URL with a [supported media type](#supported-file-types) in the `url` field of a media part.
164
+
165
+ ```json
166
+ {
167
+ "parts": [
168
+ { "type": "media", "url": "https://your-cdn.com/images/photo.jpg" }
169
+ ]
170
+ }
171
+ ```
172
+
173
+ This works with any URL you already host — no pre-upload step required. **Maximum file size: 10MB.**
174
+
175
+ ## Pre-Upload (required for files over 10MB)
176
+
177
+ Use `POST /v3/attachments` when you want to:
178
+ - **Send files larger than 10MB** (up to 100MB) — URL-based downloads are limited to 10MB
179
+ - **Send the same file to many recipients** — upload once, reuse the `attachment_id` without re-downloading each time
180
+ - **Reduce message send latency** — the file is already stored, so sending is faster
181
+
182
+ **How it works:**
183
+ 1. `POST /v3/attachments` with file metadata → returns a presigned `upload_url` (valid for **15 minutes**) and a permanent `attachment_id`
184
+ 2. PUT the raw file bytes to the `upload_url` with the `required_headers` (no JSON or multipart — just the binary content)
185
+ 3. Reference the `attachment_id` in your media part when sending messages (no expiration)
186
+
187
+ **Key difference:** When you provide an external `url`, we download and process the file on every send.
188
+ When you use a pre-uploaded `attachment_id`, the file is already stored — so repeated sends skip the download step entirely.
189
+
190
+ ## Domain Allowlisting
191
+
192
+ Attachment URLs in API responses are served from `cdn.linqapp.com`. This includes:
193
+ - `url` fields in media and voice memo message parts
194
+ - `download_url` fields in attachment and upload response objects
195
+
196
+ If your application enforces domain allowlists (e.g., for SSRF protection), add:
197
+
198
+ ```
199
+ cdn.linqapp.com
200
+ ```
201
+
202
+ ## Supported File Types
203
+
204
+ - **Images:** JPEG, PNG, GIF, HEIC, HEIF, TIFF, BMP
205
+ - **Videos:** MP4, MOV, M4V
206
+ - **Audio:** M4A, AAC, MP3, WAV, AIFF, CAF, AMR
207
+ - **Documents:** PDF, TXT, RTF, CSV, Office formats, ZIP
208
+ - **Contact & Calendar:** VCF, ICS
209
+
210
+ ## Audio: Attachment vs Voice Memo
211
+
212
+ Audio files sent as media parts appear as **downloadable file attachments** in iMessage.
213
+ To send audio as an **iMessage voice memo bubble** (with native inline playback UI),
214
+ use the dedicated `POST /v3/chats/{chatId}/voicememo` endpoint instead.
215
+
216
+ ## File Size Limits
217
+
218
+ - **URL-based (`url` field):** 10MB maximum
219
+ - **Pre-upload (`attachment_id`):** 100MB maximum
220
+ """
221
+ from .resources.attachments import AttachmentsResource
222
+
223
+ return AttachmentsResource(self)
224
+
225
+ @cached_property
226
+ def phonenumbers(self) -> PhonenumbersResource:
227
+ """Phone Numbers represent the phone numbers assigned to your partner account.
228
+
229
+ Use the list phone numbers endpoint to discover which phone numbers are available
230
+ for sending messages.
231
+
232
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
233
+ in the `from` field.
234
+ """
235
+ from .resources.phonenumbers import PhonenumbersResource
236
+
237
+ return PhonenumbersResource(self)
238
+
239
+ @cached_property
240
+ def phone_numbers(self) -> PhoneNumbersResource:
241
+ """Phone Numbers represent the phone numbers assigned to your partner account.
242
+
243
+ Use the list phone numbers endpoint to discover which phone numbers are available
244
+ for sending messages.
245
+
246
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
247
+ in the `from` field.
248
+ """
249
+ from .resources.phone_numbers import PhoneNumbersResource
250
+
251
+ return PhoneNumbersResource(self)
252
+
253
+ @cached_property
254
+ def webhook_events(self) -> WebhookEventsResource:
255
+ """
256
+ Webhook Subscriptions allow you to receive real-time notifications when events
257
+ occur on your account.
258
+
259
+ Configure webhook endpoints to receive events such as messages sent/received,
260
+ delivery status changes, reactions, typing indicators, and more.
261
+
262
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
263
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
264
+ deduplication.
265
+
266
+ ## Webhook Headers
267
+
268
+ Each webhook request includes the following headers:
269
+
270
+ | Header | Description |
271
+ |--------|-------------|
272
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
273
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
274
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
275
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
276
+
277
+ ## Verifying Webhook Signatures
278
+
279
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
280
+ to ensure the webhook originated from Linq and hasn't been tampered with.
281
+
282
+ **Signature Construction:**
283
+
284
+ The signature is computed over a concatenation of the timestamp and payload:
285
+
286
+ ```
287
+ {timestamp}.{payload}
288
+ ```
289
+
290
+ Where:
291
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
292
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
293
+
294
+ **Verification Steps:**
295
+
296
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
297
+ 2. Get the raw request body bytes (do not parse and re-serialize)
298
+ 3. Concatenate: `"{timestamp}.{payload}"`
299
+ 4. Compute HMAC-SHA256 using your signing secret as the key
300
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
301
+ 6. Use constant-time comparison to prevent timing attacks
302
+
303
+ **Example (Python):**
304
+
305
+ ```python
306
+ import hmac
307
+ import hashlib
308
+
309
+
310
+ def verify_webhook(signing_secret, payload, timestamp, signature):
311
+ message = f"{timestamp}.{payload.decode('utf-8')}"
312
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
313
+ return hmac.compare_digest(expected, signature)
314
+ ```
315
+
316
+ **Example (Node.js):**
317
+
318
+ ```javascript
319
+ const crypto = require('crypto');
320
+
321
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
322
+ const message = `${timestamp}.${payload}`;
323
+ const expected = crypto
324
+ .createHmac('sha256', signingSecret)
325
+ .update(message)
326
+ .digest('hex');
327
+ return crypto.timingSafeEqual(
328
+ Buffer.from(expected),
329
+ Buffer.from(signature)
330
+ );
331
+ }
332
+ ```
333
+
334
+ **Security Best Practices:**
335
+
336
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
337
+ - Always use constant-time comparison for signature verification
338
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
339
+ - Return a 2xx status code quickly, then process the webhook asynchronously
340
+ """
341
+ from .resources.webhook_events import WebhookEventsResource
342
+
343
+ return WebhookEventsResource(self)
344
+
345
+ @cached_property
346
+ def webhook_subscriptions(self) -> WebhookSubscriptionsResource:
347
+ """
348
+ Webhook Subscriptions allow you to receive real-time notifications when events
349
+ occur on your account.
350
+
351
+ Configure webhook endpoints to receive events such as messages sent/received,
352
+ delivery status changes, reactions, typing indicators, and more.
353
+
354
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
355
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
356
+ deduplication.
357
+
358
+ ## Webhook Headers
359
+
360
+ Each webhook request includes the following headers:
361
+
362
+ | Header | Description |
363
+ |--------|-------------|
364
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
365
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
366
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
367
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
368
+
369
+ ## Verifying Webhook Signatures
370
+
371
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
372
+ to ensure the webhook originated from Linq and hasn't been tampered with.
373
+
374
+ **Signature Construction:**
375
+
376
+ The signature is computed over a concatenation of the timestamp and payload:
377
+
378
+ ```
379
+ {timestamp}.{payload}
380
+ ```
381
+
382
+ Where:
383
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
384
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
385
+
386
+ **Verification Steps:**
387
+
388
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
389
+ 2. Get the raw request body bytes (do not parse and re-serialize)
390
+ 3. Concatenate: `"{timestamp}.{payload}"`
391
+ 4. Compute HMAC-SHA256 using your signing secret as the key
392
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
393
+ 6. Use constant-time comparison to prevent timing attacks
394
+
395
+ **Example (Python):**
396
+
397
+ ```python
398
+ import hmac
399
+ import hashlib
400
+
401
+
402
+ def verify_webhook(signing_secret, payload, timestamp, signature):
403
+ message = f"{timestamp}.{payload.decode('utf-8')}"
404
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
405
+ return hmac.compare_digest(expected, signature)
406
+ ```
407
+
408
+ **Example (Node.js):**
409
+
410
+ ```javascript
411
+ const crypto = require('crypto');
412
+
413
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
414
+ const message = `${timestamp}.${payload}`;
415
+ const expected = crypto
416
+ .createHmac('sha256', signingSecret)
417
+ .update(message)
418
+ .digest('hex');
419
+ return crypto.timingSafeEqual(
420
+ Buffer.from(expected),
421
+ Buffer.from(signature)
422
+ );
423
+ }
424
+ ```
425
+
426
+ **Security Best Practices:**
427
+
428
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
429
+ - Always use constant-time comparison for signature verification
430
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
431
+ - Return a 2xx status code quickly, then process the webhook asynchronously
432
+ """
433
+ from .resources.webhook_subscriptions import WebhookSubscriptionsResource
434
+
435
+ return WebhookSubscriptionsResource(self)
436
+
437
+ @cached_property
438
+ def capability(self) -> CapabilityResource:
439
+ """
440
+ Check whether a recipient address supports iMessage or RCS before sending a message.
441
+ """
442
+ from .resources.capability import CapabilityResource
443
+
444
+ return CapabilityResource(self)
445
+
446
+ @cached_property
447
+ def webhooks(self) -> WebhooksResource:
448
+ from .resources.webhooks import WebhooksResource
449
+
450
+ return WebhooksResource(self)
451
+
452
+ @cached_property
453
+ def contact_card(self) -> ContactCardResource:
454
+ """
455
+ Contact Card lets you set and share your contact information (name and profile photo) with chat participants via iMessage Name and Photo Sharing.
456
+
457
+ Use `POST /v3/contact_card` to create or update a card for a phone number.
458
+ Use `PATCH /v3/contact_card` to update an existing active card.
459
+ Use `GET /v3/contact_card` to retrieve the active card(s) for your partner account.
460
+
461
+ **Sharing behavior:** Sharing may not take effect in every chat due to limitations outside our control. We recommend calling the share endpoint once per day, after the first outbound activity.
462
+ """
463
+ from .resources.contact_card import ContactCardResource
464
+
465
+ return ContactCardResource(self)
466
+
467
+ @cached_property
468
+ def with_raw_response(self) -> LinqAPIV3WithRawResponse:
469
+ return LinqAPIV3WithRawResponse(self)
470
+
471
+ @cached_property
472
+ def with_streaming_response(self) -> LinqAPIV3WithStreamedResponse:
473
+ return LinqAPIV3WithStreamedResponse(self)
474
+
475
+ @property
476
+ @override
477
+ def qs(self) -> Querystring:
478
+ return Querystring(array_format="comma")
479
+
480
+ @override
481
+ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]:
482
+ return {
483
+ **(self._bearer_auth if security.get("bearer_auth", False) else {}),
484
+ }
485
+
486
+ @property
487
+ def _bearer_auth(self) -> dict[str, str]:
488
+ api_key = self.api_key
489
+ return {"Authorization": f"Bearer {api_key}"}
490
+
491
+ @property
492
+ @override
493
+ def default_headers(self) -> dict[str, str | Omit]:
494
+ return {
495
+ **super().default_headers,
496
+ "X-Stainless-Async": "false",
497
+ **self._custom_headers,
498
+ }
499
+
500
+ def copy(
501
+ self,
502
+ *,
503
+ api_key: str | None = None,
504
+ base_url: str | httpx.URL | None = None,
505
+ timeout: float | Timeout | None | NotGiven = not_given,
506
+ http_client: httpx.Client | None = None,
507
+ max_retries: int | NotGiven = not_given,
508
+ default_headers: Mapping[str, str] | None = None,
509
+ set_default_headers: Mapping[str, str] | None = None,
510
+ default_query: Mapping[str, object] | None = None,
511
+ set_default_query: Mapping[str, object] | None = None,
512
+ _extra_kwargs: Mapping[str, Any] = {},
513
+ ) -> Self:
514
+ """
515
+ Create a new client instance re-using the same options given to the current client with optional overriding.
516
+ """
517
+ if default_headers is not None and set_default_headers is not None:
518
+ raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive")
519
+
520
+ if default_query is not None and set_default_query is not None:
521
+ raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive")
522
+
523
+ headers = self._custom_headers
524
+ if default_headers is not None:
525
+ headers = {**headers, **default_headers}
526
+ elif set_default_headers is not None:
527
+ headers = set_default_headers
528
+
529
+ params = self._custom_query
530
+ if default_query is not None:
531
+ params = {**params, **default_query}
532
+ elif set_default_query is not None:
533
+ params = set_default_query
534
+
535
+ http_client = http_client or self._client
536
+ return self.__class__(
537
+ api_key=api_key or self.api_key,
538
+ base_url=base_url or self.base_url,
539
+ timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
540
+ http_client=http_client,
541
+ max_retries=max_retries if is_given(max_retries) else self.max_retries,
542
+ default_headers=headers,
543
+ default_query=params,
544
+ **_extra_kwargs,
545
+ )
546
+
547
+ # Alias for `copy` for nicer inline usage, e.g.
548
+ # client.with_options(timeout=10).foo.create(...)
549
+ with_options = copy
550
+
551
+ @override
552
+ def _make_status_error(
553
+ self,
554
+ err_msg: str,
555
+ *,
556
+ body: object,
557
+ response: httpx.Response,
558
+ ) -> APIStatusError:
559
+ if response.status_code == 400:
560
+ return _exceptions.BadRequestError(err_msg, response=response, body=body)
561
+
562
+ if response.status_code == 401:
563
+ return _exceptions.AuthenticationError(err_msg, response=response, body=body)
564
+
565
+ if response.status_code == 403:
566
+ return _exceptions.PermissionDeniedError(err_msg, response=response, body=body)
567
+
568
+ if response.status_code == 404:
569
+ return _exceptions.NotFoundError(err_msg, response=response, body=body)
570
+
571
+ if response.status_code == 409:
572
+ return _exceptions.ConflictError(err_msg, response=response, body=body)
573
+
574
+ if response.status_code == 422:
575
+ return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body)
576
+
577
+ if response.status_code == 429:
578
+ return _exceptions.RateLimitError(err_msg, response=response, body=body)
579
+
580
+ if response.status_code >= 500:
581
+ return _exceptions.InternalServerError(err_msg, response=response, body=body)
582
+ return APIStatusError(err_msg, response=response, body=body)
583
+
584
+
585
+ class AsyncLinqAPIV3(AsyncAPIClient):
586
+ # client options
587
+ api_key: str
588
+
589
+ def __init__(
590
+ self,
591
+ *,
592
+ api_key: str | None = None,
593
+ base_url: str | httpx.URL | None = None,
594
+ timeout: float | Timeout | None | NotGiven = not_given,
595
+ max_retries: int = DEFAULT_MAX_RETRIES,
596
+ default_headers: Mapping[str, str] | None = None,
597
+ default_query: Mapping[str, object] | None = None,
598
+ # Configure a custom httpx client.
599
+ # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`.
600
+ # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details.
601
+ http_client: httpx.AsyncClient | None = None,
602
+ # Enable or disable schema validation for data returned by the API.
603
+ # When enabled an error APIResponseValidationError is raised
604
+ # if the API responds with invalid data for the expected schema.
605
+ #
606
+ # This parameter may be removed or changed in the future.
607
+ # If you rely on this feature, please open a GitHub issue
608
+ # outlining your use-case to help us decide if it should be
609
+ # part of our public interface in the future.
610
+ _strict_response_validation: bool = False,
611
+ ) -> None:
612
+ """Construct a new async AsyncLinqAPIV3 client instance.
613
+
614
+ This automatically infers the `api_key` argument from the `LINQ_API_V3_API_KEY` environment variable if it is not provided.
615
+ """
616
+ if api_key is None:
617
+ api_key = os.environ.get("LINQ_API_V3_API_KEY")
618
+ if api_key is None:
619
+ raise LinqApiv3Error(
620
+ "The api_key client option must be set either by passing api_key to the client or by setting the LINQ_API_V3_API_KEY environment variable"
621
+ )
622
+ self.api_key = api_key
623
+
624
+ if base_url is None:
625
+ base_url = os.environ.get("LINQ_API_V3_BASE_URL")
626
+ if base_url is None:
627
+ base_url = f"https://api.linqapp.com/api/partner"
628
+
629
+ super().__init__(
630
+ version=__version__,
631
+ base_url=base_url,
632
+ max_retries=max_retries,
633
+ timeout=timeout,
634
+ http_client=http_client,
635
+ custom_headers=default_headers,
636
+ custom_query=default_query,
637
+ _strict_response_validation=_strict_response_validation,
638
+ )
639
+
640
+ @cached_property
641
+ def chats(self) -> AsyncChatsResource:
642
+ from .resources.chats import AsyncChatsResource
643
+
644
+ return AsyncChatsResource(self)
645
+
646
+ @cached_property
647
+ def messages(self) -> AsyncMessagesResource:
648
+ """Messages are individual communications within a chat thread.
649
+
650
+ Messages can include text, media attachments, rich link previews, special effects
651
+ (like confetti or fireworks), and reactions. All messages are associated with a
652
+ specific chat and sent from a phone number you own.
653
+
654
+ Messages support delivery status tracking, read receipts, and editing capabilities.
655
+
656
+ ## Rich Link Previews
657
+
658
+ Send a URL as a `link` part to deliver it with a rich preview card showing the
659
+ page's title, description, and image (when available). A `link` part must be the
660
+ **only** part in the message — it cannot be combined with text or media parts.
661
+ To send a URL without a preview card, include it in a `text` part instead.
662
+
663
+ **Limitations:**
664
+ - A `link` part cannot be combined with other parts in the same message.
665
+ - Maximum URL length: 2,048 characters.
666
+ """
667
+ from .resources.messages import AsyncMessagesResource
668
+
669
+ return AsyncMessagesResource(self)
670
+
671
+ @cached_property
672
+ def attachments(self) -> AsyncAttachmentsResource:
673
+ """
674
+ Send files (images, videos, documents, audio) with messages by providing a URL in a media part.
675
+ Pre-uploading via `POST /v3/attachments` is **optional** and only needed for specific optimization scenarios.
676
+
677
+ ## Sending Media via URL (up to 10MB)
678
+
679
+ Provide a publicly accessible HTTPS URL with a [supported media type](#supported-file-types) in the `url` field of a media part.
680
+
681
+ ```json
682
+ {
683
+ "parts": [
684
+ { "type": "media", "url": "https://your-cdn.com/images/photo.jpg" }
685
+ ]
686
+ }
687
+ ```
688
+
689
+ This works with any URL you already host — no pre-upload step required. **Maximum file size: 10MB.**
690
+
691
+ ## Pre-Upload (required for files over 10MB)
692
+
693
+ Use `POST /v3/attachments` when you want to:
694
+ - **Send files larger than 10MB** (up to 100MB) — URL-based downloads are limited to 10MB
695
+ - **Send the same file to many recipients** — upload once, reuse the `attachment_id` without re-downloading each time
696
+ - **Reduce message send latency** — the file is already stored, so sending is faster
697
+
698
+ **How it works:**
699
+ 1. `POST /v3/attachments` with file metadata → returns a presigned `upload_url` (valid for **15 minutes**) and a permanent `attachment_id`
700
+ 2. PUT the raw file bytes to the `upload_url` with the `required_headers` (no JSON or multipart — just the binary content)
701
+ 3. Reference the `attachment_id` in your media part when sending messages (no expiration)
702
+
703
+ **Key difference:** When you provide an external `url`, we download and process the file on every send.
704
+ When you use a pre-uploaded `attachment_id`, the file is already stored — so repeated sends skip the download step entirely.
705
+
706
+ ## Domain Allowlisting
707
+
708
+ Attachment URLs in API responses are served from `cdn.linqapp.com`. This includes:
709
+ - `url` fields in media and voice memo message parts
710
+ - `download_url` fields in attachment and upload response objects
711
+
712
+ If your application enforces domain allowlists (e.g., for SSRF protection), add:
713
+
714
+ ```
715
+ cdn.linqapp.com
716
+ ```
717
+
718
+ ## Supported File Types
719
+
720
+ - **Images:** JPEG, PNG, GIF, HEIC, HEIF, TIFF, BMP
721
+ - **Videos:** MP4, MOV, M4V
722
+ - **Audio:** M4A, AAC, MP3, WAV, AIFF, CAF, AMR
723
+ - **Documents:** PDF, TXT, RTF, CSV, Office formats, ZIP
724
+ - **Contact & Calendar:** VCF, ICS
725
+
726
+ ## Audio: Attachment vs Voice Memo
727
+
728
+ Audio files sent as media parts appear as **downloadable file attachments** in iMessage.
729
+ To send audio as an **iMessage voice memo bubble** (with native inline playback UI),
730
+ use the dedicated `POST /v3/chats/{chatId}/voicememo` endpoint instead.
731
+
732
+ ## File Size Limits
733
+
734
+ - **URL-based (`url` field):** 10MB maximum
735
+ - **Pre-upload (`attachment_id`):** 100MB maximum
736
+ """
737
+ from .resources.attachments import AsyncAttachmentsResource
738
+
739
+ return AsyncAttachmentsResource(self)
740
+
741
+ @cached_property
742
+ def phonenumbers(self) -> AsyncPhonenumbersResource:
743
+ """Phone Numbers represent the phone numbers assigned to your partner account.
744
+
745
+ Use the list phone numbers endpoint to discover which phone numbers are available
746
+ for sending messages.
747
+
748
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
749
+ in the `from` field.
750
+ """
751
+ from .resources.phonenumbers import AsyncPhonenumbersResource
752
+
753
+ return AsyncPhonenumbersResource(self)
754
+
755
+ @cached_property
756
+ def phone_numbers(self) -> AsyncPhoneNumbersResource:
757
+ """Phone Numbers represent the phone numbers assigned to your partner account.
758
+
759
+ Use the list phone numbers endpoint to discover which phone numbers are available
760
+ for sending messages.
761
+
762
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
763
+ in the `from` field.
764
+ """
765
+ from .resources.phone_numbers import AsyncPhoneNumbersResource
766
+
767
+ return AsyncPhoneNumbersResource(self)
768
+
769
+ @cached_property
770
+ def webhook_events(self) -> AsyncWebhookEventsResource:
771
+ """
772
+ Webhook Subscriptions allow you to receive real-time notifications when events
773
+ occur on your account.
774
+
775
+ Configure webhook endpoints to receive events such as messages sent/received,
776
+ delivery status changes, reactions, typing indicators, and more.
777
+
778
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
779
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
780
+ deduplication.
781
+
782
+ ## Webhook Headers
783
+
784
+ Each webhook request includes the following headers:
785
+
786
+ | Header | Description |
787
+ |--------|-------------|
788
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
789
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
790
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
791
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
792
+
793
+ ## Verifying Webhook Signatures
794
+
795
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
796
+ to ensure the webhook originated from Linq and hasn't been tampered with.
797
+
798
+ **Signature Construction:**
799
+
800
+ The signature is computed over a concatenation of the timestamp and payload:
801
+
802
+ ```
803
+ {timestamp}.{payload}
804
+ ```
805
+
806
+ Where:
807
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
808
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
809
+
810
+ **Verification Steps:**
811
+
812
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
813
+ 2. Get the raw request body bytes (do not parse and re-serialize)
814
+ 3. Concatenate: `"{timestamp}.{payload}"`
815
+ 4. Compute HMAC-SHA256 using your signing secret as the key
816
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
817
+ 6. Use constant-time comparison to prevent timing attacks
818
+
819
+ **Example (Python):**
820
+
821
+ ```python
822
+ import hmac
823
+ import hashlib
824
+
825
+
826
+ def verify_webhook(signing_secret, payload, timestamp, signature):
827
+ message = f"{timestamp}.{payload.decode('utf-8')}"
828
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
829
+ return hmac.compare_digest(expected, signature)
830
+ ```
831
+
832
+ **Example (Node.js):**
833
+
834
+ ```javascript
835
+ const crypto = require('crypto');
836
+
837
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
838
+ const message = `${timestamp}.${payload}`;
839
+ const expected = crypto
840
+ .createHmac('sha256', signingSecret)
841
+ .update(message)
842
+ .digest('hex');
843
+ return crypto.timingSafeEqual(
844
+ Buffer.from(expected),
845
+ Buffer.from(signature)
846
+ );
847
+ }
848
+ ```
849
+
850
+ **Security Best Practices:**
851
+
852
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
853
+ - Always use constant-time comparison for signature verification
854
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
855
+ - Return a 2xx status code quickly, then process the webhook asynchronously
856
+ """
857
+ from .resources.webhook_events import AsyncWebhookEventsResource
858
+
859
+ return AsyncWebhookEventsResource(self)
860
+
861
+ @cached_property
862
+ def webhook_subscriptions(self) -> AsyncWebhookSubscriptionsResource:
863
+ """
864
+ Webhook Subscriptions allow you to receive real-time notifications when events
865
+ occur on your account.
866
+
867
+ Configure webhook endpoints to receive events such as messages sent/received,
868
+ delivery status changes, reactions, typing indicators, and more.
869
+
870
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
871
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
872
+ deduplication.
873
+
874
+ ## Webhook Headers
875
+
876
+ Each webhook request includes the following headers:
877
+
878
+ | Header | Description |
879
+ |--------|-------------|
880
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
881
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
882
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
883
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
884
+
885
+ ## Verifying Webhook Signatures
886
+
887
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
888
+ to ensure the webhook originated from Linq and hasn't been tampered with.
889
+
890
+ **Signature Construction:**
891
+
892
+ The signature is computed over a concatenation of the timestamp and payload:
893
+
894
+ ```
895
+ {timestamp}.{payload}
896
+ ```
897
+
898
+ Where:
899
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
900
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
901
+
902
+ **Verification Steps:**
903
+
904
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
905
+ 2. Get the raw request body bytes (do not parse and re-serialize)
906
+ 3. Concatenate: `"{timestamp}.{payload}"`
907
+ 4. Compute HMAC-SHA256 using your signing secret as the key
908
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
909
+ 6. Use constant-time comparison to prevent timing attacks
910
+
911
+ **Example (Python):**
912
+
913
+ ```python
914
+ import hmac
915
+ import hashlib
916
+
917
+
918
+ def verify_webhook(signing_secret, payload, timestamp, signature):
919
+ message = f"{timestamp}.{payload.decode('utf-8')}"
920
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
921
+ return hmac.compare_digest(expected, signature)
922
+ ```
923
+
924
+ **Example (Node.js):**
925
+
926
+ ```javascript
927
+ const crypto = require('crypto');
928
+
929
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
930
+ const message = `${timestamp}.${payload}`;
931
+ const expected = crypto
932
+ .createHmac('sha256', signingSecret)
933
+ .update(message)
934
+ .digest('hex');
935
+ return crypto.timingSafeEqual(
936
+ Buffer.from(expected),
937
+ Buffer.from(signature)
938
+ );
939
+ }
940
+ ```
941
+
942
+ **Security Best Practices:**
943
+
944
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
945
+ - Always use constant-time comparison for signature verification
946
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
947
+ - Return a 2xx status code quickly, then process the webhook asynchronously
948
+ """
949
+ from .resources.webhook_subscriptions import AsyncWebhookSubscriptionsResource
950
+
951
+ return AsyncWebhookSubscriptionsResource(self)
952
+
953
+ @cached_property
954
+ def capability(self) -> AsyncCapabilityResource:
955
+ """
956
+ Check whether a recipient address supports iMessage or RCS before sending a message.
957
+ """
958
+ from .resources.capability import AsyncCapabilityResource
959
+
960
+ return AsyncCapabilityResource(self)
961
+
962
+ @cached_property
963
+ def webhooks(self) -> AsyncWebhooksResource:
964
+ from .resources.webhooks import AsyncWebhooksResource
965
+
966
+ return AsyncWebhooksResource(self)
967
+
968
+ @cached_property
969
+ def contact_card(self) -> AsyncContactCardResource:
970
+ """
971
+ Contact Card lets you set and share your contact information (name and profile photo) with chat participants via iMessage Name and Photo Sharing.
972
+
973
+ Use `POST /v3/contact_card` to create or update a card for a phone number.
974
+ Use `PATCH /v3/contact_card` to update an existing active card.
975
+ Use `GET /v3/contact_card` to retrieve the active card(s) for your partner account.
976
+
977
+ **Sharing behavior:** Sharing may not take effect in every chat due to limitations outside our control. We recommend calling the share endpoint once per day, after the first outbound activity.
978
+ """
979
+ from .resources.contact_card import AsyncContactCardResource
980
+
981
+ return AsyncContactCardResource(self)
982
+
983
+ @cached_property
984
+ def with_raw_response(self) -> AsyncLinqAPIV3WithRawResponse:
985
+ return AsyncLinqAPIV3WithRawResponse(self)
986
+
987
+ @cached_property
988
+ def with_streaming_response(self) -> AsyncLinqAPIV3WithStreamedResponse:
989
+ return AsyncLinqAPIV3WithStreamedResponse(self)
990
+
991
+ @property
992
+ @override
993
+ def qs(self) -> Querystring:
994
+ return Querystring(array_format="comma")
995
+
996
+ @override
997
+ def _auth_headers(self, security: SecurityOptions) -> dict[str, str]:
998
+ return {
999
+ **(self._bearer_auth if security.get("bearer_auth", False) else {}),
1000
+ }
1001
+
1002
+ @property
1003
+ def _bearer_auth(self) -> dict[str, str]:
1004
+ api_key = self.api_key
1005
+ return {"Authorization": f"Bearer {api_key}"}
1006
+
1007
+ @property
1008
+ @override
1009
+ def default_headers(self) -> dict[str, str | Omit]:
1010
+ return {
1011
+ **super().default_headers,
1012
+ "X-Stainless-Async": f"async:{get_async_library()}",
1013
+ **self._custom_headers,
1014
+ }
1015
+
1016
+ def copy(
1017
+ self,
1018
+ *,
1019
+ api_key: str | None = None,
1020
+ base_url: str | httpx.URL | None = None,
1021
+ timeout: float | Timeout | None | NotGiven = not_given,
1022
+ http_client: httpx.AsyncClient | None = None,
1023
+ max_retries: int | NotGiven = not_given,
1024
+ default_headers: Mapping[str, str] | None = None,
1025
+ set_default_headers: Mapping[str, str] | None = None,
1026
+ default_query: Mapping[str, object] | None = None,
1027
+ set_default_query: Mapping[str, object] | None = None,
1028
+ _extra_kwargs: Mapping[str, Any] = {},
1029
+ ) -> Self:
1030
+ """
1031
+ Create a new client instance re-using the same options given to the current client with optional overriding.
1032
+ """
1033
+ if default_headers is not None and set_default_headers is not None:
1034
+ raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive")
1035
+
1036
+ if default_query is not None and set_default_query is not None:
1037
+ raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive")
1038
+
1039
+ headers = self._custom_headers
1040
+ if default_headers is not None:
1041
+ headers = {**headers, **default_headers}
1042
+ elif set_default_headers is not None:
1043
+ headers = set_default_headers
1044
+
1045
+ params = self._custom_query
1046
+ if default_query is not None:
1047
+ params = {**params, **default_query}
1048
+ elif set_default_query is not None:
1049
+ params = set_default_query
1050
+
1051
+ http_client = http_client or self._client
1052
+ return self.__class__(
1053
+ api_key=api_key or self.api_key,
1054
+ base_url=base_url or self.base_url,
1055
+ timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
1056
+ http_client=http_client,
1057
+ max_retries=max_retries if is_given(max_retries) else self.max_retries,
1058
+ default_headers=headers,
1059
+ default_query=params,
1060
+ **_extra_kwargs,
1061
+ )
1062
+
1063
+ # Alias for `copy` for nicer inline usage, e.g.
1064
+ # client.with_options(timeout=10).foo.create(...)
1065
+ with_options = copy
1066
+
1067
+ @override
1068
+ def _make_status_error(
1069
+ self,
1070
+ err_msg: str,
1071
+ *,
1072
+ body: object,
1073
+ response: httpx.Response,
1074
+ ) -> APIStatusError:
1075
+ if response.status_code == 400:
1076
+ return _exceptions.BadRequestError(err_msg, response=response, body=body)
1077
+
1078
+ if response.status_code == 401:
1079
+ return _exceptions.AuthenticationError(err_msg, response=response, body=body)
1080
+
1081
+ if response.status_code == 403:
1082
+ return _exceptions.PermissionDeniedError(err_msg, response=response, body=body)
1083
+
1084
+ if response.status_code == 404:
1085
+ return _exceptions.NotFoundError(err_msg, response=response, body=body)
1086
+
1087
+ if response.status_code == 409:
1088
+ return _exceptions.ConflictError(err_msg, response=response, body=body)
1089
+
1090
+ if response.status_code == 422:
1091
+ return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body)
1092
+
1093
+ if response.status_code == 429:
1094
+ return _exceptions.RateLimitError(err_msg, response=response, body=body)
1095
+
1096
+ if response.status_code >= 500:
1097
+ return _exceptions.InternalServerError(err_msg, response=response, body=body)
1098
+ return APIStatusError(err_msg, response=response, body=body)
1099
+
1100
+
1101
+ class LinqAPIV3WithRawResponse:
1102
+ _client: LinqAPIV3
1103
+
1104
+ def __init__(self, client: LinqAPIV3) -> None:
1105
+ self._client = client
1106
+
1107
+ @cached_property
1108
+ def chats(self) -> chats.ChatsResourceWithRawResponse:
1109
+ from .resources.chats import ChatsResourceWithRawResponse
1110
+
1111
+ return ChatsResourceWithRawResponse(self._client.chats)
1112
+
1113
+ @cached_property
1114
+ def messages(self) -> messages.MessagesResourceWithRawResponse:
1115
+ """Messages are individual communications within a chat thread.
1116
+
1117
+ Messages can include text, media attachments, rich link previews, special effects
1118
+ (like confetti or fireworks), and reactions. All messages are associated with a
1119
+ specific chat and sent from a phone number you own.
1120
+
1121
+ Messages support delivery status tracking, read receipts, and editing capabilities.
1122
+
1123
+ ## Rich Link Previews
1124
+
1125
+ Send a URL as a `link` part to deliver it with a rich preview card showing the
1126
+ page's title, description, and image (when available). A `link` part must be the
1127
+ **only** part in the message — it cannot be combined with text or media parts.
1128
+ To send a URL without a preview card, include it in a `text` part instead.
1129
+
1130
+ **Limitations:**
1131
+ - A `link` part cannot be combined with other parts in the same message.
1132
+ - Maximum URL length: 2,048 characters.
1133
+ """
1134
+ from .resources.messages import MessagesResourceWithRawResponse
1135
+
1136
+ return MessagesResourceWithRawResponse(self._client.messages)
1137
+
1138
+ @cached_property
1139
+ def attachments(self) -> attachments.AttachmentsResourceWithRawResponse:
1140
+ """
1141
+ Send files (images, videos, documents, audio) with messages by providing a URL in a media part.
1142
+ Pre-uploading via `POST /v3/attachments` is **optional** and only needed for specific optimization scenarios.
1143
+
1144
+ ## Sending Media via URL (up to 10MB)
1145
+
1146
+ Provide a publicly accessible HTTPS URL with a [supported media type](#supported-file-types) in the `url` field of a media part.
1147
+
1148
+ ```json
1149
+ {
1150
+ "parts": [
1151
+ { "type": "media", "url": "https://your-cdn.com/images/photo.jpg" }
1152
+ ]
1153
+ }
1154
+ ```
1155
+
1156
+ This works with any URL you already host — no pre-upload step required. **Maximum file size: 10MB.**
1157
+
1158
+ ## Pre-Upload (required for files over 10MB)
1159
+
1160
+ Use `POST /v3/attachments` when you want to:
1161
+ - **Send files larger than 10MB** (up to 100MB) — URL-based downloads are limited to 10MB
1162
+ - **Send the same file to many recipients** — upload once, reuse the `attachment_id` without re-downloading each time
1163
+ - **Reduce message send latency** — the file is already stored, so sending is faster
1164
+
1165
+ **How it works:**
1166
+ 1. `POST /v3/attachments` with file metadata → returns a presigned `upload_url` (valid for **15 minutes**) and a permanent `attachment_id`
1167
+ 2. PUT the raw file bytes to the `upload_url` with the `required_headers` (no JSON or multipart — just the binary content)
1168
+ 3. Reference the `attachment_id` in your media part when sending messages (no expiration)
1169
+
1170
+ **Key difference:** When you provide an external `url`, we download and process the file on every send.
1171
+ When you use a pre-uploaded `attachment_id`, the file is already stored — so repeated sends skip the download step entirely.
1172
+
1173
+ ## Domain Allowlisting
1174
+
1175
+ Attachment URLs in API responses are served from `cdn.linqapp.com`. This includes:
1176
+ - `url` fields in media and voice memo message parts
1177
+ - `download_url` fields in attachment and upload response objects
1178
+
1179
+ If your application enforces domain allowlists (e.g., for SSRF protection), add:
1180
+
1181
+ ```
1182
+ cdn.linqapp.com
1183
+ ```
1184
+
1185
+ ## Supported File Types
1186
+
1187
+ - **Images:** JPEG, PNG, GIF, HEIC, HEIF, TIFF, BMP
1188
+ - **Videos:** MP4, MOV, M4V
1189
+ - **Audio:** M4A, AAC, MP3, WAV, AIFF, CAF, AMR
1190
+ - **Documents:** PDF, TXT, RTF, CSV, Office formats, ZIP
1191
+ - **Contact & Calendar:** VCF, ICS
1192
+
1193
+ ## Audio: Attachment vs Voice Memo
1194
+
1195
+ Audio files sent as media parts appear as **downloadable file attachments** in iMessage.
1196
+ To send audio as an **iMessage voice memo bubble** (with native inline playback UI),
1197
+ use the dedicated `POST /v3/chats/{chatId}/voicememo` endpoint instead.
1198
+
1199
+ ## File Size Limits
1200
+
1201
+ - **URL-based (`url` field):** 10MB maximum
1202
+ - **Pre-upload (`attachment_id`):** 100MB maximum
1203
+ """
1204
+ from .resources.attachments import AttachmentsResourceWithRawResponse
1205
+
1206
+ return AttachmentsResourceWithRawResponse(self._client.attachments)
1207
+
1208
+ @cached_property
1209
+ def phonenumbers(self) -> phonenumbers.PhonenumbersResourceWithRawResponse:
1210
+ """Phone Numbers represent the phone numbers assigned to your partner account.
1211
+
1212
+ Use the list phone numbers endpoint to discover which phone numbers are available
1213
+ for sending messages.
1214
+
1215
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
1216
+ in the `from` field.
1217
+ """
1218
+ from .resources.phonenumbers import PhonenumbersResourceWithRawResponse
1219
+
1220
+ return PhonenumbersResourceWithRawResponse(self._client.phonenumbers)
1221
+
1222
+ @cached_property
1223
+ def phone_numbers(self) -> phone_numbers.PhoneNumbersResourceWithRawResponse:
1224
+ """Phone Numbers represent the phone numbers assigned to your partner account.
1225
+
1226
+ Use the list phone numbers endpoint to discover which phone numbers are available
1227
+ for sending messages.
1228
+
1229
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
1230
+ in the `from` field.
1231
+ """
1232
+ from .resources.phone_numbers import PhoneNumbersResourceWithRawResponse
1233
+
1234
+ return PhoneNumbersResourceWithRawResponse(self._client.phone_numbers)
1235
+
1236
+ @cached_property
1237
+ def webhook_events(self) -> webhook_events.WebhookEventsResourceWithRawResponse:
1238
+ """
1239
+ Webhook Subscriptions allow you to receive real-time notifications when events
1240
+ occur on your account.
1241
+
1242
+ Configure webhook endpoints to receive events such as messages sent/received,
1243
+ delivery status changes, reactions, typing indicators, and more.
1244
+
1245
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
1246
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
1247
+ deduplication.
1248
+
1249
+ ## Webhook Headers
1250
+
1251
+ Each webhook request includes the following headers:
1252
+
1253
+ | Header | Description |
1254
+ |--------|-------------|
1255
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
1256
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
1257
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
1258
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
1259
+
1260
+ ## Verifying Webhook Signatures
1261
+
1262
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
1263
+ to ensure the webhook originated from Linq and hasn't been tampered with.
1264
+
1265
+ **Signature Construction:**
1266
+
1267
+ The signature is computed over a concatenation of the timestamp and payload:
1268
+
1269
+ ```
1270
+ {timestamp}.{payload}
1271
+ ```
1272
+
1273
+ Where:
1274
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
1275
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
1276
+
1277
+ **Verification Steps:**
1278
+
1279
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
1280
+ 2. Get the raw request body bytes (do not parse and re-serialize)
1281
+ 3. Concatenate: `"{timestamp}.{payload}"`
1282
+ 4. Compute HMAC-SHA256 using your signing secret as the key
1283
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
1284
+ 6. Use constant-time comparison to prevent timing attacks
1285
+
1286
+ **Example (Python):**
1287
+
1288
+ ```python
1289
+ import hmac
1290
+ import hashlib
1291
+
1292
+
1293
+ def verify_webhook(signing_secret, payload, timestamp, signature):
1294
+ message = f"{timestamp}.{payload.decode('utf-8')}"
1295
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
1296
+ return hmac.compare_digest(expected, signature)
1297
+ ```
1298
+
1299
+ **Example (Node.js):**
1300
+
1301
+ ```javascript
1302
+ const crypto = require('crypto');
1303
+
1304
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
1305
+ const message = `${timestamp}.${payload}`;
1306
+ const expected = crypto
1307
+ .createHmac('sha256', signingSecret)
1308
+ .update(message)
1309
+ .digest('hex');
1310
+ return crypto.timingSafeEqual(
1311
+ Buffer.from(expected),
1312
+ Buffer.from(signature)
1313
+ );
1314
+ }
1315
+ ```
1316
+
1317
+ **Security Best Practices:**
1318
+
1319
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
1320
+ - Always use constant-time comparison for signature verification
1321
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
1322
+ - Return a 2xx status code quickly, then process the webhook asynchronously
1323
+ """
1324
+ from .resources.webhook_events import WebhookEventsResourceWithRawResponse
1325
+
1326
+ return WebhookEventsResourceWithRawResponse(self._client.webhook_events)
1327
+
1328
+ @cached_property
1329
+ def webhook_subscriptions(self) -> webhook_subscriptions.WebhookSubscriptionsResourceWithRawResponse:
1330
+ """
1331
+ Webhook Subscriptions allow you to receive real-time notifications when events
1332
+ occur on your account.
1333
+
1334
+ Configure webhook endpoints to receive events such as messages sent/received,
1335
+ delivery status changes, reactions, typing indicators, and more.
1336
+
1337
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
1338
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
1339
+ deduplication.
1340
+
1341
+ ## Webhook Headers
1342
+
1343
+ Each webhook request includes the following headers:
1344
+
1345
+ | Header | Description |
1346
+ |--------|-------------|
1347
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
1348
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
1349
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
1350
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
1351
+
1352
+ ## Verifying Webhook Signatures
1353
+
1354
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
1355
+ to ensure the webhook originated from Linq and hasn't been tampered with.
1356
+
1357
+ **Signature Construction:**
1358
+
1359
+ The signature is computed over a concatenation of the timestamp and payload:
1360
+
1361
+ ```
1362
+ {timestamp}.{payload}
1363
+ ```
1364
+
1365
+ Where:
1366
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
1367
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
1368
+
1369
+ **Verification Steps:**
1370
+
1371
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
1372
+ 2. Get the raw request body bytes (do not parse and re-serialize)
1373
+ 3. Concatenate: `"{timestamp}.{payload}"`
1374
+ 4. Compute HMAC-SHA256 using your signing secret as the key
1375
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
1376
+ 6. Use constant-time comparison to prevent timing attacks
1377
+
1378
+ **Example (Python):**
1379
+
1380
+ ```python
1381
+ import hmac
1382
+ import hashlib
1383
+
1384
+
1385
+ def verify_webhook(signing_secret, payload, timestamp, signature):
1386
+ message = f"{timestamp}.{payload.decode('utf-8')}"
1387
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
1388
+ return hmac.compare_digest(expected, signature)
1389
+ ```
1390
+
1391
+ **Example (Node.js):**
1392
+
1393
+ ```javascript
1394
+ const crypto = require('crypto');
1395
+
1396
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
1397
+ const message = `${timestamp}.${payload}`;
1398
+ const expected = crypto
1399
+ .createHmac('sha256', signingSecret)
1400
+ .update(message)
1401
+ .digest('hex');
1402
+ return crypto.timingSafeEqual(
1403
+ Buffer.from(expected),
1404
+ Buffer.from(signature)
1405
+ );
1406
+ }
1407
+ ```
1408
+
1409
+ **Security Best Practices:**
1410
+
1411
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
1412
+ - Always use constant-time comparison for signature verification
1413
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
1414
+ - Return a 2xx status code quickly, then process the webhook asynchronously
1415
+ """
1416
+ from .resources.webhook_subscriptions import WebhookSubscriptionsResourceWithRawResponse
1417
+
1418
+ return WebhookSubscriptionsResourceWithRawResponse(self._client.webhook_subscriptions)
1419
+
1420
+ @cached_property
1421
+ def capability(self) -> capability.CapabilityResourceWithRawResponse:
1422
+ """
1423
+ Check whether a recipient address supports iMessage or RCS before sending a message.
1424
+ """
1425
+ from .resources.capability import CapabilityResourceWithRawResponse
1426
+
1427
+ return CapabilityResourceWithRawResponse(self._client.capability)
1428
+
1429
+ @cached_property
1430
+ def contact_card(self) -> contact_card.ContactCardResourceWithRawResponse:
1431
+ """
1432
+ Contact Card lets you set and share your contact information (name and profile photo) with chat participants via iMessage Name and Photo Sharing.
1433
+
1434
+ Use `POST /v3/contact_card` to create or update a card for a phone number.
1435
+ Use `PATCH /v3/contact_card` to update an existing active card.
1436
+ Use `GET /v3/contact_card` to retrieve the active card(s) for your partner account.
1437
+
1438
+ **Sharing behavior:** Sharing may not take effect in every chat due to limitations outside our control. We recommend calling the share endpoint once per day, after the first outbound activity.
1439
+ """
1440
+ from .resources.contact_card import ContactCardResourceWithRawResponse
1441
+
1442
+ return ContactCardResourceWithRawResponse(self._client.contact_card)
1443
+
1444
+
1445
+ class AsyncLinqAPIV3WithRawResponse:
1446
+ _client: AsyncLinqAPIV3
1447
+
1448
+ def __init__(self, client: AsyncLinqAPIV3) -> None:
1449
+ self._client = client
1450
+
1451
+ @cached_property
1452
+ def chats(self) -> chats.AsyncChatsResourceWithRawResponse:
1453
+ from .resources.chats import AsyncChatsResourceWithRawResponse
1454
+
1455
+ return AsyncChatsResourceWithRawResponse(self._client.chats)
1456
+
1457
+ @cached_property
1458
+ def messages(self) -> messages.AsyncMessagesResourceWithRawResponse:
1459
+ """Messages are individual communications within a chat thread.
1460
+
1461
+ Messages can include text, media attachments, rich link previews, special effects
1462
+ (like confetti or fireworks), and reactions. All messages are associated with a
1463
+ specific chat and sent from a phone number you own.
1464
+
1465
+ Messages support delivery status tracking, read receipts, and editing capabilities.
1466
+
1467
+ ## Rich Link Previews
1468
+
1469
+ Send a URL as a `link` part to deliver it with a rich preview card showing the
1470
+ page's title, description, and image (when available). A `link` part must be the
1471
+ **only** part in the message — it cannot be combined with text or media parts.
1472
+ To send a URL without a preview card, include it in a `text` part instead.
1473
+
1474
+ **Limitations:**
1475
+ - A `link` part cannot be combined with other parts in the same message.
1476
+ - Maximum URL length: 2,048 characters.
1477
+ """
1478
+ from .resources.messages import AsyncMessagesResourceWithRawResponse
1479
+
1480
+ return AsyncMessagesResourceWithRawResponse(self._client.messages)
1481
+
1482
+ @cached_property
1483
+ def attachments(self) -> attachments.AsyncAttachmentsResourceWithRawResponse:
1484
+ """
1485
+ Send files (images, videos, documents, audio) with messages by providing a URL in a media part.
1486
+ Pre-uploading via `POST /v3/attachments` is **optional** and only needed for specific optimization scenarios.
1487
+
1488
+ ## Sending Media via URL (up to 10MB)
1489
+
1490
+ Provide a publicly accessible HTTPS URL with a [supported media type](#supported-file-types) in the `url` field of a media part.
1491
+
1492
+ ```json
1493
+ {
1494
+ "parts": [
1495
+ { "type": "media", "url": "https://your-cdn.com/images/photo.jpg" }
1496
+ ]
1497
+ }
1498
+ ```
1499
+
1500
+ This works with any URL you already host — no pre-upload step required. **Maximum file size: 10MB.**
1501
+
1502
+ ## Pre-Upload (required for files over 10MB)
1503
+
1504
+ Use `POST /v3/attachments` when you want to:
1505
+ - **Send files larger than 10MB** (up to 100MB) — URL-based downloads are limited to 10MB
1506
+ - **Send the same file to many recipients** — upload once, reuse the `attachment_id` without re-downloading each time
1507
+ - **Reduce message send latency** — the file is already stored, so sending is faster
1508
+
1509
+ **How it works:**
1510
+ 1. `POST /v3/attachments` with file metadata → returns a presigned `upload_url` (valid for **15 minutes**) and a permanent `attachment_id`
1511
+ 2. PUT the raw file bytes to the `upload_url` with the `required_headers` (no JSON or multipart — just the binary content)
1512
+ 3. Reference the `attachment_id` in your media part when sending messages (no expiration)
1513
+
1514
+ **Key difference:** When you provide an external `url`, we download and process the file on every send.
1515
+ When you use a pre-uploaded `attachment_id`, the file is already stored — so repeated sends skip the download step entirely.
1516
+
1517
+ ## Domain Allowlisting
1518
+
1519
+ Attachment URLs in API responses are served from `cdn.linqapp.com`. This includes:
1520
+ - `url` fields in media and voice memo message parts
1521
+ - `download_url` fields in attachment and upload response objects
1522
+
1523
+ If your application enforces domain allowlists (e.g., for SSRF protection), add:
1524
+
1525
+ ```
1526
+ cdn.linqapp.com
1527
+ ```
1528
+
1529
+ ## Supported File Types
1530
+
1531
+ - **Images:** JPEG, PNG, GIF, HEIC, HEIF, TIFF, BMP
1532
+ - **Videos:** MP4, MOV, M4V
1533
+ - **Audio:** M4A, AAC, MP3, WAV, AIFF, CAF, AMR
1534
+ - **Documents:** PDF, TXT, RTF, CSV, Office formats, ZIP
1535
+ - **Contact & Calendar:** VCF, ICS
1536
+
1537
+ ## Audio: Attachment vs Voice Memo
1538
+
1539
+ Audio files sent as media parts appear as **downloadable file attachments** in iMessage.
1540
+ To send audio as an **iMessage voice memo bubble** (with native inline playback UI),
1541
+ use the dedicated `POST /v3/chats/{chatId}/voicememo` endpoint instead.
1542
+
1543
+ ## File Size Limits
1544
+
1545
+ - **URL-based (`url` field):** 10MB maximum
1546
+ - **Pre-upload (`attachment_id`):** 100MB maximum
1547
+ """
1548
+ from .resources.attachments import AsyncAttachmentsResourceWithRawResponse
1549
+
1550
+ return AsyncAttachmentsResourceWithRawResponse(self._client.attachments)
1551
+
1552
+ @cached_property
1553
+ def phonenumbers(self) -> phonenumbers.AsyncPhonenumbersResourceWithRawResponse:
1554
+ """Phone Numbers represent the phone numbers assigned to your partner account.
1555
+
1556
+ Use the list phone numbers endpoint to discover which phone numbers are available
1557
+ for sending messages.
1558
+
1559
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
1560
+ in the `from` field.
1561
+ """
1562
+ from .resources.phonenumbers import AsyncPhonenumbersResourceWithRawResponse
1563
+
1564
+ return AsyncPhonenumbersResourceWithRawResponse(self._client.phonenumbers)
1565
+
1566
+ @cached_property
1567
+ def phone_numbers(self) -> phone_numbers.AsyncPhoneNumbersResourceWithRawResponse:
1568
+ """Phone Numbers represent the phone numbers assigned to your partner account.
1569
+
1570
+ Use the list phone numbers endpoint to discover which phone numbers are available
1571
+ for sending messages.
1572
+
1573
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
1574
+ in the `from` field.
1575
+ """
1576
+ from .resources.phone_numbers import AsyncPhoneNumbersResourceWithRawResponse
1577
+
1578
+ return AsyncPhoneNumbersResourceWithRawResponse(self._client.phone_numbers)
1579
+
1580
+ @cached_property
1581
+ def webhook_events(self) -> webhook_events.AsyncWebhookEventsResourceWithRawResponse:
1582
+ """
1583
+ Webhook Subscriptions allow you to receive real-time notifications when events
1584
+ occur on your account.
1585
+
1586
+ Configure webhook endpoints to receive events such as messages sent/received,
1587
+ delivery status changes, reactions, typing indicators, and more.
1588
+
1589
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
1590
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
1591
+ deduplication.
1592
+
1593
+ ## Webhook Headers
1594
+
1595
+ Each webhook request includes the following headers:
1596
+
1597
+ | Header | Description |
1598
+ |--------|-------------|
1599
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
1600
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
1601
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
1602
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
1603
+
1604
+ ## Verifying Webhook Signatures
1605
+
1606
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
1607
+ to ensure the webhook originated from Linq and hasn't been tampered with.
1608
+
1609
+ **Signature Construction:**
1610
+
1611
+ The signature is computed over a concatenation of the timestamp and payload:
1612
+
1613
+ ```
1614
+ {timestamp}.{payload}
1615
+ ```
1616
+
1617
+ Where:
1618
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
1619
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
1620
+
1621
+ **Verification Steps:**
1622
+
1623
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
1624
+ 2. Get the raw request body bytes (do not parse and re-serialize)
1625
+ 3. Concatenate: `"{timestamp}.{payload}"`
1626
+ 4. Compute HMAC-SHA256 using your signing secret as the key
1627
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
1628
+ 6. Use constant-time comparison to prevent timing attacks
1629
+
1630
+ **Example (Python):**
1631
+
1632
+ ```python
1633
+ import hmac
1634
+ import hashlib
1635
+
1636
+
1637
+ def verify_webhook(signing_secret, payload, timestamp, signature):
1638
+ message = f"{timestamp}.{payload.decode('utf-8')}"
1639
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
1640
+ return hmac.compare_digest(expected, signature)
1641
+ ```
1642
+
1643
+ **Example (Node.js):**
1644
+
1645
+ ```javascript
1646
+ const crypto = require('crypto');
1647
+
1648
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
1649
+ const message = `${timestamp}.${payload}`;
1650
+ const expected = crypto
1651
+ .createHmac('sha256', signingSecret)
1652
+ .update(message)
1653
+ .digest('hex');
1654
+ return crypto.timingSafeEqual(
1655
+ Buffer.from(expected),
1656
+ Buffer.from(signature)
1657
+ );
1658
+ }
1659
+ ```
1660
+
1661
+ **Security Best Practices:**
1662
+
1663
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
1664
+ - Always use constant-time comparison for signature verification
1665
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
1666
+ - Return a 2xx status code quickly, then process the webhook asynchronously
1667
+ """
1668
+ from .resources.webhook_events import AsyncWebhookEventsResourceWithRawResponse
1669
+
1670
+ return AsyncWebhookEventsResourceWithRawResponse(self._client.webhook_events)
1671
+
1672
+ @cached_property
1673
+ def webhook_subscriptions(self) -> webhook_subscriptions.AsyncWebhookSubscriptionsResourceWithRawResponse:
1674
+ """
1675
+ Webhook Subscriptions allow you to receive real-time notifications when events
1676
+ occur on your account.
1677
+
1678
+ Configure webhook endpoints to receive events such as messages sent/received,
1679
+ delivery status changes, reactions, typing indicators, and more.
1680
+
1681
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
1682
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
1683
+ deduplication.
1684
+
1685
+ ## Webhook Headers
1686
+
1687
+ Each webhook request includes the following headers:
1688
+
1689
+ | Header | Description |
1690
+ |--------|-------------|
1691
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
1692
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
1693
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
1694
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
1695
+
1696
+ ## Verifying Webhook Signatures
1697
+
1698
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
1699
+ to ensure the webhook originated from Linq and hasn't been tampered with.
1700
+
1701
+ **Signature Construction:**
1702
+
1703
+ The signature is computed over a concatenation of the timestamp and payload:
1704
+
1705
+ ```
1706
+ {timestamp}.{payload}
1707
+ ```
1708
+
1709
+ Where:
1710
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
1711
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
1712
+
1713
+ **Verification Steps:**
1714
+
1715
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
1716
+ 2. Get the raw request body bytes (do not parse and re-serialize)
1717
+ 3. Concatenate: `"{timestamp}.{payload}"`
1718
+ 4. Compute HMAC-SHA256 using your signing secret as the key
1719
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
1720
+ 6. Use constant-time comparison to prevent timing attacks
1721
+
1722
+ **Example (Python):**
1723
+
1724
+ ```python
1725
+ import hmac
1726
+ import hashlib
1727
+
1728
+
1729
+ def verify_webhook(signing_secret, payload, timestamp, signature):
1730
+ message = f"{timestamp}.{payload.decode('utf-8')}"
1731
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
1732
+ return hmac.compare_digest(expected, signature)
1733
+ ```
1734
+
1735
+ **Example (Node.js):**
1736
+
1737
+ ```javascript
1738
+ const crypto = require('crypto');
1739
+
1740
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
1741
+ const message = `${timestamp}.${payload}`;
1742
+ const expected = crypto
1743
+ .createHmac('sha256', signingSecret)
1744
+ .update(message)
1745
+ .digest('hex');
1746
+ return crypto.timingSafeEqual(
1747
+ Buffer.from(expected),
1748
+ Buffer.from(signature)
1749
+ );
1750
+ }
1751
+ ```
1752
+
1753
+ **Security Best Practices:**
1754
+
1755
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
1756
+ - Always use constant-time comparison for signature verification
1757
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
1758
+ - Return a 2xx status code quickly, then process the webhook asynchronously
1759
+ """
1760
+ from .resources.webhook_subscriptions import AsyncWebhookSubscriptionsResourceWithRawResponse
1761
+
1762
+ return AsyncWebhookSubscriptionsResourceWithRawResponse(self._client.webhook_subscriptions)
1763
+
1764
+ @cached_property
1765
+ def capability(self) -> capability.AsyncCapabilityResourceWithRawResponse:
1766
+ """
1767
+ Check whether a recipient address supports iMessage or RCS before sending a message.
1768
+ """
1769
+ from .resources.capability import AsyncCapabilityResourceWithRawResponse
1770
+
1771
+ return AsyncCapabilityResourceWithRawResponse(self._client.capability)
1772
+
1773
+ @cached_property
1774
+ def contact_card(self) -> contact_card.AsyncContactCardResourceWithRawResponse:
1775
+ """
1776
+ Contact Card lets you set and share your contact information (name and profile photo) with chat participants via iMessage Name and Photo Sharing.
1777
+
1778
+ Use `POST /v3/contact_card` to create or update a card for a phone number.
1779
+ Use `PATCH /v3/contact_card` to update an existing active card.
1780
+ Use `GET /v3/contact_card` to retrieve the active card(s) for your partner account.
1781
+
1782
+ **Sharing behavior:** Sharing may not take effect in every chat due to limitations outside our control. We recommend calling the share endpoint once per day, after the first outbound activity.
1783
+ """
1784
+ from .resources.contact_card import AsyncContactCardResourceWithRawResponse
1785
+
1786
+ return AsyncContactCardResourceWithRawResponse(self._client.contact_card)
1787
+
1788
+
1789
+ class LinqAPIV3WithStreamedResponse:
1790
+ _client: LinqAPIV3
1791
+
1792
+ def __init__(self, client: LinqAPIV3) -> None:
1793
+ self._client = client
1794
+
1795
+ @cached_property
1796
+ def chats(self) -> chats.ChatsResourceWithStreamingResponse:
1797
+ from .resources.chats import ChatsResourceWithStreamingResponse
1798
+
1799
+ return ChatsResourceWithStreamingResponse(self._client.chats)
1800
+
1801
+ @cached_property
1802
+ def messages(self) -> messages.MessagesResourceWithStreamingResponse:
1803
+ """Messages are individual communications within a chat thread.
1804
+
1805
+ Messages can include text, media attachments, rich link previews, special effects
1806
+ (like confetti or fireworks), and reactions. All messages are associated with a
1807
+ specific chat and sent from a phone number you own.
1808
+
1809
+ Messages support delivery status tracking, read receipts, and editing capabilities.
1810
+
1811
+ ## Rich Link Previews
1812
+
1813
+ Send a URL as a `link` part to deliver it with a rich preview card showing the
1814
+ page's title, description, and image (when available). A `link` part must be the
1815
+ **only** part in the message — it cannot be combined with text or media parts.
1816
+ To send a URL without a preview card, include it in a `text` part instead.
1817
+
1818
+ **Limitations:**
1819
+ - A `link` part cannot be combined with other parts in the same message.
1820
+ - Maximum URL length: 2,048 characters.
1821
+ """
1822
+ from .resources.messages import MessagesResourceWithStreamingResponse
1823
+
1824
+ return MessagesResourceWithStreamingResponse(self._client.messages)
1825
+
1826
+ @cached_property
1827
+ def attachments(self) -> attachments.AttachmentsResourceWithStreamingResponse:
1828
+ """
1829
+ Send files (images, videos, documents, audio) with messages by providing a URL in a media part.
1830
+ Pre-uploading via `POST /v3/attachments` is **optional** and only needed for specific optimization scenarios.
1831
+
1832
+ ## Sending Media via URL (up to 10MB)
1833
+
1834
+ Provide a publicly accessible HTTPS URL with a [supported media type](#supported-file-types) in the `url` field of a media part.
1835
+
1836
+ ```json
1837
+ {
1838
+ "parts": [
1839
+ { "type": "media", "url": "https://your-cdn.com/images/photo.jpg" }
1840
+ ]
1841
+ }
1842
+ ```
1843
+
1844
+ This works with any URL you already host — no pre-upload step required. **Maximum file size: 10MB.**
1845
+
1846
+ ## Pre-Upload (required for files over 10MB)
1847
+
1848
+ Use `POST /v3/attachments` when you want to:
1849
+ - **Send files larger than 10MB** (up to 100MB) — URL-based downloads are limited to 10MB
1850
+ - **Send the same file to many recipients** — upload once, reuse the `attachment_id` without re-downloading each time
1851
+ - **Reduce message send latency** — the file is already stored, so sending is faster
1852
+
1853
+ **How it works:**
1854
+ 1. `POST /v3/attachments` with file metadata → returns a presigned `upload_url` (valid for **15 minutes**) and a permanent `attachment_id`
1855
+ 2. PUT the raw file bytes to the `upload_url` with the `required_headers` (no JSON or multipart — just the binary content)
1856
+ 3. Reference the `attachment_id` in your media part when sending messages (no expiration)
1857
+
1858
+ **Key difference:** When you provide an external `url`, we download and process the file on every send.
1859
+ When you use a pre-uploaded `attachment_id`, the file is already stored — so repeated sends skip the download step entirely.
1860
+
1861
+ ## Domain Allowlisting
1862
+
1863
+ Attachment URLs in API responses are served from `cdn.linqapp.com`. This includes:
1864
+ - `url` fields in media and voice memo message parts
1865
+ - `download_url` fields in attachment and upload response objects
1866
+
1867
+ If your application enforces domain allowlists (e.g., for SSRF protection), add:
1868
+
1869
+ ```
1870
+ cdn.linqapp.com
1871
+ ```
1872
+
1873
+ ## Supported File Types
1874
+
1875
+ - **Images:** JPEG, PNG, GIF, HEIC, HEIF, TIFF, BMP
1876
+ - **Videos:** MP4, MOV, M4V
1877
+ - **Audio:** M4A, AAC, MP3, WAV, AIFF, CAF, AMR
1878
+ - **Documents:** PDF, TXT, RTF, CSV, Office formats, ZIP
1879
+ - **Contact & Calendar:** VCF, ICS
1880
+
1881
+ ## Audio: Attachment vs Voice Memo
1882
+
1883
+ Audio files sent as media parts appear as **downloadable file attachments** in iMessage.
1884
+ To send audio as an **iMessage voice memo bubble** (with native inline playback UI),
1885
+ use the dedicated `POST /v3/chats/{chatId}/voicememo` endpoint instead.
1886
+
1887
+ ## File Size Limits
1888
+
1889
+ - **URL-based (`url` field):** 10MB maximum
1890
+ - **Pre-upload (`attachment_id`):** 100MB maximum
1891
+ """
1892
+ from .resources.attachments import AttachmentsResourceWithStreamingResponse
1893
+
1894
+ return AttachmentsResourceWithStreamingResponse(self._client.attachments)
1895
+
1896
+ @cached_property
1897
+ def phonenumbers(self) -> phonenumbers.PhonenumbersResourceWithStreamingResponse:
1898
+ """Phone Numbers represent the phone numbers assigned to your partner account.
1899
+
1900
+ Use the list phone numbers endpoint to discover which phone numbers are available
1901
+ for sending messages.
1902
+
1903
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
1904
+ in the `from` field.
1905
+ """
1906
+ from .resources.phonenumbers import PhonenumbersResourceWithStreamingResponse
1907
+
1908
+ return PhonenumbersResourceWithStreamingResponse(self._client.phonenumbers)
1909
+
1910
+ @cached_property
1911
+ def phone_numbers(self) -> phone_numbers.PhoneNumbersResourceWithStreamingResponse:
1912
+ """Phone Numbers represent the phone numbers assigned to your partner account.
1913
+
1914
+ Use the list phone numbers endpoint to discover which phone numbers are available
1915
+ for sending messages.
1916
+
1917
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
1918
+ in the `from` field.
1919
+ """
1920
+ from .resources.phone_numbers import PhoneNumbersResourceWithStreamingResponse
1921
+
1922
+ return PhoneNumbersResourceWithStreamingResponse(self._client.phone_numbers)
1923
+
1924
+ @cached_property
1925
+ def webhook_events(self) -> webhook_events.WebhookEventsResourceWithStreamingResponse:
1926
+ """
1927
+ Webhook Subscriptions allow you to receive real-time notifications when events
1928
+ occur on your account.
1929
+
1930
+ Configure webhook endpoints to receive events such as messages sent/received,
1931
+ delivery status changes, reactions, typing indicators, and more.
1932
+
1933
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
1934
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
1935
+ deduplication.
1936
+
1937
+ ## Webhook Headers
1938
+
1939
+ Each webhook request includes the following headers:
1940
+
1941
+ | Header | Description |
1942
+ |--------|-------------|
1943
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
1944
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
1945
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
1946
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
1947
+
1948
+ ## Verifying Webhook Signatures
1949
+
1950
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
1951
+ to ensure the webhook originated from Linq and hasn't been tampered with.
1952
+
1953
+ **Signature Construction:**
1954
+
1955
+ The signature is computed over a concatenation of the timestamp and payload:
1956
+
1957
+ ```
1958
+ {timestamp}.{payload}
1959
+ ```
1960
+
1961
+ Where:
1962
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
1963
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
1964
+
1965
+ **Verification Steps:**
1966
+
1967
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
1968
+ 2. Get the raw request body bytes (do not parse and re-serialize)
1969
+ 3. Concatenate: `"{timestamp}.{payload}"`
1970
+ 4. Compute HMAC-SHA256 using your signing secret as the key
1971
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
1972
+ 6. Use constant-time comparison to prevent timing attacks
1973
+
1974
+ **Example (Python):**
1975
+
1976
+ ```python
1977
+ import hmac
1978
+ import hashlib
1979
+
1980
+
1981
+ def verify_webhook(signing_secret, payload, timestamp, signature):
1982
+ message = f"{timestamp}.{payload.decode('utf-8')}"
1983
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
1984
+ return hmac.compare_digest(expected, signature)
1985
+ ```
1986
+
1987
+ **Example (Node.js):**
1988
+
1989
+ ```javascript
1990
+ const crypto = require('crypto');
1991
+
1992
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
1993
+ const message = `${timestamp}.${payload}`;
1994
+ const expected = crypto
1995
+ .createHmac('sha256', signingSecret)
1996
+ .update(message)
1997
+ .digest('hex');
1998
+ return crypto.timingSafeEqual(
1999
+ Buffer.from(expected),
2000
+ Buffer.from(signature)
2001
+ );
2002
+ }
2003
+ ```
2004
+
2005
+ **Security Best Practices:**
2006
+
2007
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
2008
+ - Always use constant-time comparison for signature verification
2009
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
2010
+ - Return a 2xx status code quickly, then process the webhook asynchronously
2011
+ """
2012
+ from .resources.webhook_events import WebhookEventsResourceWithStreamingResponse
2013
+
2014
+ return WebhookEventsResourceWithStreamingResponse(self._client.webhook_events)
2015
+
2016
+ @cached_property
2017
+ def webhook_subscriptions(self) -> webhook_subscriptions.WebhookSubscriptionsResourceWithStreamingResponse:
2018
+ """
2019
+ Webhook Subscriptions allow you to receive real-time notifications when events
2020
+ occur on your account.
2021
+
2022
+ Configure webhook endpoints to receive events such as messages sent/received,
2023
+ delivery status changes, reactions, typing indicators, and more.
2024
+
2025
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
2026
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
2027
+ deduplication.
2028
+
2029
+ ## Webhook Headers
2030
+
2031
+ Each webhook request includes the following headers:
2032
+
2033
+ | Header | Description |
2034
+ |--------|-------------|
2035
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
2036
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
2037
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
2038
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
2039
+
2040
+ ## Verifying Webhook Signatures
2041
+
2042
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
2043
+ to ensure the webhook originated from Linq and hasn't been tampered with.
2044
+
2045
+ **Signature Construction:**
2046
+
2047
+ The signature is computed over a concatenation of the timestamp and payload:
2048
+
2049
+ ```
2050
+ {timestamp}.{payload}
2051
+ ```
2052
+
2053
+ Where:
2054
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
2055
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
2056
+
2057
+ **Verification Steps:**
2058
+
2059
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
2060
+ 2. Get the raw request body bytes (do not parse and re-serialize)
2061
+ 3. Concatenate: `"{timestamp}.{payload}"`
2062
+ 4. Compute HMAC-SHA256 using your signing secret as the key
2063
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
2064
+ 6. Use constant-time comparison to prevent timing attacks
2065
+
2066
+ **Example (Python):**
2067
+
2068
+ ```python
2069
+ import hmac
2070
+ import hashlib
2071
+
2072
+
2073
+ def verify_webhook(signing_secret, payload, timestamp, signature):
2074
+ message = f"{timestamp}.{payload.decode('utf-8')}"
2075
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
2076
+ return hmac.compare_digest(expected, signature)
2077
+ ```
2078
+
2079
+ **Example (Node.js):**
2080
+
2081
+ ```javascript
2082
+ const crypto = require('crypto');
2083
+
2084
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
2085
+ const message = `${timestamp}.${payload}`;
2086
+ const expected = crypto
2087
+ .createHmac('sha256', signingSecret)
2088
+ .update(message)
2089
+ .digest('hex');
2090
+ return crypto.timingSafeEqual(
2091
+ Buffer.from(expected),
2092
+ Buffer.from(signature)
2093
+ );
2094
+ }
2095
+ ```
2096
+
2097
+ **Security Best Practices:**
2098
+
2099
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
2100
+ - Always use constant-time comparison for signature verification
2101
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
2102
+ - Return a 2xx status code quickly, then process the webhook asynchronously
2103
+ """
2104
+ from .resources.webhook_subscriptions import WebhookSubscriptionsResourceWithStreamingResponse
2105
+
2106
+ return WebhookSubscriptionsResourceWithStreamingResponse(self._client.webhook_subscriptions)
2107
+
2108
+ @cached_property
2109
+ def capability(self) -> capability.CapabilityResourceWithStreamingResponse:
2110
+ """
2111
+ Check whether a recipient address supports iMessage or RCS before sending a message.
2112
+ """
2113
+ from .resources.capability import CapabilityResourceWithStreamingResponse
2114
+
2115
+ return CapabilityResourceWithStreamingResponse(self._client.capability)
2116
+
2117
+ @cached_property
2118
+ def contact_card(self) -> contact_card.ContactCardResourceWithStreamingResponse:
2119
+ """
2120
+ Contact Card lets you set and share your contact information (name and profile photo) with chat participants via iMessage Name and Photo Sharing.
2121
+
2122
+ Use `POST /v3/contact_card` to create or update a card for a phone number.
2123
+ Use `PATCH /v3/contact_card` to update an existing active card.
2124
+ Use `GET /v3/contact_card` to retrieve the active card(s) for your partner account.
2125
+
2126
+ **Sharing behavior:** Sharing may not take effect in every chat due to limitations outside our control. We recommend calling the share endpoint once per day, after the first outbound activity.
2127
+ """
2128
+ from .resources.contact_card import ContactCardResourceWithStreamingResponse
2129
+
2130
+ return ContactCardResourceWithStreamingResponse(self._client.contact_card)
2131
+
2132
+
2133
+ class AsyncLinqAPIV3WithStreamedResponse:
2134
+ _client: AsyncLinqAPIV3
2135
+
2136
+ def __init__(self, client: AsyncLinqAPIV3) -> None:
2137
+ self._client = client
2138
+
2139
+ @cached_property
2140
+ def chats(self) -> chats.AsyncChatsResourceWithStreamingResponse:
2141
+ from .resources.chats import AsyncChatsResourceWithStreamingResponse
2142
+
2143
+ return AsyncChatsResourceWithStreamingResponse(self._client.chats)
2144
+
2145
+ @cached_property
2146
+ def messages(self) -> messages.AsyncMessagesResourceWithStreamingResponse:
2147
+ """Messages are individual communications within a chat thread.
2148
+
2149
+ Messages can include text, media attachments, rich link previews, special effects
2150
+ (like confetti or fireworks), and reactions. All messages are associated with a
2151
+ specific chat and sent from a phone number you own.
2152
+
2153
+ Messages support delivery status tracking, read receipts, and editing capabilities.
2154
+
2155
+ ## Rich Link Previews
2156
+
2157
+ Send a URL as a `link` part to deliver it with a rich preview card showing the
2158
+ page's title, description, and image (when available). A `link` part must be the
2159
+ **only** part in the message — it cannot be combined with text or media parts.
2160
+ To send a URL without a preview card, include it in a `text` part instead.
2161
+
2162
+ **Limitations:**
2163
+ - A `link` part cannot be combined with other parts in the same message.
2164
+ - Maximum URL length: 2,048 characters.
2165
+ """
2166
+ from .resources.messages import AsyncMessagesResourceWithStreamingResponse
2167
+
2168
+ return AsyncMessagesResourceWithStreamingResponse(self._client.messages)
2169
+
2170
+ @cached_property
2171
+ def attachments(self) -> attachments.AsyncAttachmentsResourceWithStreamingResponse:
2172
+ """
2173
+ Send files (images, videos, documents, audio) with messages by providing a URL in a media part.
2174
+ Pre-uploading via `POST /v3/attachments` is **optional** and only needed for specific optimization scenarios.
2175
+
2176
+ ## Sending Media via URL (up to 10MB)
2177
+
2178
+ Provide a publicly accessible HTTPS URL with a [supported media type](#supported-file-types) in the `url` field of a media part.
2179
+
2180
+ ```json
2181
+ {
2182
+ "parts": [
2183
+ { "type": "media", "url": "https://your-cdn.com/images/photo.jpg" }
2184
+ ]
2185
+ }
2186
+ ```
2187
+
2188
+ This works with any URL you already host — no pre-upload step required. **Maximum file size: 10MB.**
2189
+
2190
+ ## Pre-Upload (required for files over 10MB)
2191
+
2192
+ Use `POST /v3/attachments` when you want to:
2193
+ - **Send files larger than 10MB** (up to 100MB) — URL-based downloads are limited to 10MB
2194
+ - **Send the same file to many recipients** — upload once, reuse the `attachment_id` without re-downloading each time
2195
+ - **Reduce message send latency** — the file is already stored, so sending is faster
2196
+
2197
+ **How it works:**
2198
+ 1. `POST /v3/attachments` with file metadata → returns a presigned `upload_url` (valid for **15 minutes**) and a permanent `attachment_id`
2199
+ 2. PUT the raw file bytes to the `upload_url` with the `required_headers` (no JSON or multipart — just the binary content)
2200
+ 3. Reference the `attachment_id` in your media part when sending messages (no expiration)
2201
+
2202
+ **Key difference:** When you provide an external `url`, we download and process the file on every send.
2203
+ When you use a pre-uploaded `attachment_id`, the file is already stored — so repeated sends skip the download step entirely.
2204
+
2205
+ ## Domain Allowlisting
2206
+
2207
+ Attachment URLs in API responses are served from `cdn.linqapp.com`. This includes:
2208
+ - `url` fields in media and voice memo message parts
2209
+ - `download_url` fields in attachment and upload response objects
2210
+
2211
+ If your application enforces domain allowlists (e.g., for SSRF protection), add:
2212
+
2213
+ ```
2214
+ cdn.linqapp.com
2215
+ ```
2216
+
2217
+ ## Supported File Types
2218
+
2219
+ - **Images:** JPEG, PNG, GIF, HEIC, HEIF, TIFF, BMP
2220
+ - **Videos:** MP4, MOV, M4V
2221
+ - **Audio:** M4A, AAC, MP3, WAV, AIFF, CAF, AMR
2222
+ - **Documents:** PDF, TXT, RTF, CSV, Office formats, ZIP
2223
+ - **Contact & Calendar:** VCF, ICS
2224
+
2225
+ ## Audio: Attachment vs Voice Memo
2226
+
2227
+ Audio files sent as media parts appear as **downloadable file attachments** in iMessage.
2228
+ To send audio as an **iMessage voice memo bubble** (with native inline playback UI),
2229
+ use the dedicated `POST /v3/chats/{chatId}/voicememo` endpoint instead.
2230
+
2231
+ ## File Size Limits
2232
+
2233
+ - **URL-based (`url` field):** 10MB maximum
2234
+ - **Pre-upload (`attachment_id`):** 100MB maximum
2235
+ """
2236
+ from .resources.attachments import AsyncAttachmentsResourceWithStreamingResponse
2237
+
2238
+ return AsyncAttachmentsResourceWithStreamingResponse(self._client.attachments)
2239
+
2240
+ @cached_property
2241
+ def phonenumbers(self) -> phonenumbers.AsyncPhonenumbersResourceWithStreamingResponse:
2242
+ """Phone Numbers represent the phone numbers assigned to your partner account.
2243
+
2244
+ Use the list phone numbers endpoint to discover which phone numbers are available
2245
+ for sending messages.
2246
+
2247
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
2248
+ in the `from` field.
2249
+ """
2250
+ from .resources.phonenumbers import AsyncPhonenumbersResourceWithStreamingResponse
2251
+
2252
+ return AsyncPhonenumbersResourceWithStreamingResponse(self._client.phonenumbers)
2253
+
2254
+ @cached_property
2255
+ def phone_numbers(self) -> phone_numbers.AsyncPhoneNumbersResourceWithStreamingResponse:
2256
+ """Phone Numbers represent the phone numbers assigned to your partner account.
2257
+
2258
+ Use the list phone numbers endpoint to discover which phone numbers are available
2259
+ for sending messages.
2260
+
2261
+ When creating chats, listing chats, or sending a voice memo, use one of your assigned phone numbers
2262
+ in the `from` field.
2263
+ """
2264
+ from .resources.phone_numbers import AsyncPhoneNumbersResourceWithStreamingResponse
2265
+
2266
+ return AsyncPhoneNumbersResourceWithStreamingResponse(self._client.phone_numbers)
2267
+
2268
+ @cached_property
2269
+ def webhook_events(self) -> webhook_events.AsyncWebhookEventsResourceWithStreamingResponse:
2270
+ """
2271
+ Webhook Subscriptions allow you to receive real-time notifications when events
2272
+ occur on your account.
2273
+
2274
+ Configure webhook endpoints to receive events such as messages sent/received,
2275
+ delivery status changes, reactions, typing indicators, and more.
2276
+
2277
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
2278
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
2279
+ deduplication.
2280
+
2281
+ ## Webhook Headers
2282
+
2283
+ Each webhook request includes the following headers:
2284
+
2285
+ | Header | Description |
2286
+ |--------|-------------|
2287
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
2288
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
2289
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
2290
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
2291
+
2292
+ ## Verifying Webhook Signatures
2293
+
2294
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
2295
+ to ensure the webhook originated from Linq and hasn't been tampered with.
2296
+
2297
+ **Signature Construction:**
2298
+
2299
+ The signature is computed over a concatenation of the timestamp and payload:
2300
+
2301
+ ```
2302
+ {timestamp}.{payload}
2303
+ ```
2304
+
2305
+ Where:
2306
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
2307
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
2308
+
2309
+ **Verification Steps:**
2310
+
2311
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
2312
+ 2. Get the raw request body bytes (do not parse and re-serialize)
2313
+ 3. Concatenate: `"{timestamp}.{payload}"`
2314
+ 4. Compute HMAC-SHA256 using your signing secret as the key
2315
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
2316
+ 6. Use constant-time comparison to prevent timing attacks
2317
+
2318
+ **Example (Python):**
2319
+
2320
+ ```python
2321
+ import hmac
2322
+ import hashlib
2323
+
2324
+
2325
+ def verify_webhook(signing_secret, payload, timestamp, signature):
2326
+ message = f"{timestamp}.{payload.decode('utf-8')}"
2327
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
2328
+ return hmac.compare_digest(expected, signature)
2329
+ ```
2330
+
2331
+ **Example (Node.js):**
2332
+
2333
+ ```javascript
2334
+ const crypto = require('crypto');
2335
+
2336
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
2337
+ const message = `${timestamp}.${payload}`;
2338
+ const expected = crypto
2339
+ .createHmac('sha256', signingSecret)
2340
+ .update(message)
2341
+ .digest('hex');
2342
+ return crypto.timingSafeEqual(
2343
+ Buffer.from(expected),
2344
+ Buffer.from(signature)
2345
+ );
2346
+ }
2347
+ ```
2348
+
2349
+ **Security Best Practices:**
2350
+
2351
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
2352
+ - Always use constant-time comparison for signature verification
2353
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
2354
+ - Return a 2xx status code quickly, then process the webhook asynchronously
2355
+ """
2356
+ from .resources.webhook_events import AsyncWebhookEventsResourceWithStreamingResponse
2357
+
2358
+ return AsyncWebhookEventsResourceWithStreamingResponse(self._client.webhook_events)
2359
+
2360
+ @cached_property
2361
+ def webhook_subscriptions(self) -> webhook_subscriptions.AsyncWebhookSubscriptionsResourceWithStreamingResponse:
2362
+ """
2363
+ Webhook Subscriptions allow you to receive real-time notifications when events
2364
+ occur on your account.
2365
+
2366
+ Configure webhook endpoints to receive events such as messages sent/received,
2367
+ delivery status changes, reactions, typing indicators, and more.
2368
+
2369
+ Failed deliveries (5xx, 429, network errors) are retried up to 10 times over
2370
+ ~25 minutes with exponential backoff. Each event includes a unique ID for
2371
+ deduplication.
2372
+
2373
+ ## Webhook Headers
2374
+
2375
+ Each webhook request includes the following headers:
2376
+
2377
+ | Header | Description |
2378
+ |--------|-------------|
2379
+ | `X-Webhook-Event` | The event type (e.g., `message.sent`, `message.received`) |
2380
+ | `X-Webhook-Subscription-ID` | Your webhook subscription ID |
2381
+ | `X-Webhook-Timestamp` | Unix timestamp (seconds) when the webhook was sent |
2382
+ | `X-Webhook-Signature` | HMAC-SHA256 signature for verification |
2383
+
2384
+ ## Verifying Webhook Signatures
2385
+
2386
+ All webhooks are signed using HMAC-SHA256. You should always verify the signature
2387
+ to ensure the webhook originated from Linq and hasn't been tampered with.
2388
+
2389
+ **Signature Construction:**
2390
+
2391
+ The signature is computed over a concatenation of the timestamp and payload:
2392
+
2393
+ ```
2394
+ {timestamp}.{payload}
2395
+ ```
2396
+
2397
+ Where:
2398
+ - `timestamp` is the value from the `X-Webhook-Timestamp` header
2399
+ - `payload` is the raw JSON request body (exact bytes, not re-serialized)
2400
+
2401
+ **Verification Steps:**
2402
+
2403
+ 1. Extract the `X-Webhook-Timestamp` and `X-Webhook-Signature` headers
2404
+ 2. Get the raw request body bytes (do not parse and re-serialize)
2405
+ 3. Concatenate: `"{timestamp}.{payload}"`
2406
+ 4. Compute HMAC-SHA256 using your signing secret as the key
2407
+ 5. Hex-encode the result and compare with `X-Webhook-Signature`
2408
+ 6. Use constant-time comparison to prevent timing attacks
2409
+
2410
+ **Example (Python):**
2411
+
2412
+ ```python
2413
+ import hmac
2414
+ import hashlib
2415
+
2416
+
2417
+ def verify_webhook(signing_secret, payload, timestamp, signature):
2418
+ message = f"{timestamp}.{payload.decode('utf-8')}"
2419
+ expected = hmac.new(signing_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
2420
+ return hmac.compare_digest(expected, signature)
2421
+ ```
2422
+
2423
+ **Example (Node.js):**
2424
+
2425
+ ```javascript
2426
+ const crypto = require('crypto');
2427
+
2428
+ function verifyWebhook(signingSecret, payload, timestamp, signature) {
2429
+ const message = `${timestamp}.${payload}`;
2430
+ const expected = crypto
2431
+ .createHmac('sha256', signingSecret)
2432
+ .update(message)
2433
+ .digest('hex');
2434
+ return crypto.timingSafeEqual(
2435
+ Buffer.from(expected),
2436
+ Buffer.from(signature)
2437
+ );
2438
+ }
2439
+ ```
2440
+
2441
+ **Security Best Practices:**
2442
+
2443
+ - Reject webhooks with timestamps older than 5 minutes to prevent replay attacks
2444
+ - Always use constant-time comparison for signature verification
2445
+ - Store your signing secret securely (e.g., environment variable, secrets manager)
2446
+ - Return a 2xx status code quickly, then process the webhook asynchronously
2447
+ """
2448
+ from .resources.webhook_subscriptions import AsyncWebhookSubscriptionsResourceWithStreamingResponse
2449
+
2450
+ return AsyncWebhookSubscriptionsResourceWithStreamingResponse(self._client.webhook_subscriptions)
2451
+
2452
+ @cached_property
2453
+ def capability(self) -> capability.AsyncCapabilityResourceWithStreamingResponse:
2454
+ """
2455
+ Check whether a recipient address supports iMessage or RCS before sending a message.
2456
+ """
2457
+ from .resources.capability import AsyncCapabilityResourceWithStreamingResponse
2458
+
2459
+ return AsyncCapabilityResourceWithStreamingResponse(self._client.capability)
2460
+
2461
+ @cached_property
2462
+ def contact_card(self) -> contact_card.AsyncContactCardResourceWithStreamingResponse:
2463
+ """
2464
+ Contact Card lets you set and share your contact information (name and profile photo) with chat participants via iMessage Name and Photo Sharing.
2465
+
2466
+ Use `POST /v3/contact_card` to create or update a card for a phone number.
2467
+ Use `PATCH /v3/contact_card` to update an existing active card.
2468
+ Use `GET /v3/contact_card` to retrieve the active card(s) for your partner account.
2469
+
2470
+ **Sharing behavior:** Sharing may not take effect in every chat due to limitations outside our control. We recommend calling the share endpoint once per day, after the first outbound activity.
2471
+ """
2472
+ from .resources.contact_card import AsyncContactCardResourceWithStreamingResponse
2473
+
2474
+ return AsyncContactCardResourceWithStreamingResponse(self._client.contact_card)
2475
+
2476
+
2477
+ Client = LinqAPIV3
2478
+
2479
+ AsyncClient = AsyncLinqAPIV3