From 410c32eb25514c01459f4d24d7ae7442d1da9277 Mon Sep 17 00:00:00 2001 From: linq-sdks-bot Date: Tue, 4 Aug 2026 18:06:03 +0000 Subject: [PATCH] fix: flatten poll subresources to avoid go compile errors --- .stats.yml | 2 +- api.md | 45 +- packages/mcp-server/src/code-tool-worker.ts | 4 + packages/mcp-server/src/local-docs-search.ts | 178 +++- packages/mcp-server/src/methods.ts | 24 + packages/mcp-server/src/server.ts | 2 +- src/client.ts | 30 +- src/core/error.ts | 21 +- src/resources/chats/chats.ts | 103 +- src/resources/chats/index.ts | 1 + src/resources/chats/messages.ts | 20 +- src/resources/chats/polls.ts | 196 ++++ src/resources/index.ts | 2 +- src/resources/messages.ts | 933 +---------------- src/resources/messages/index.ts | 18 + src/resources/messages/messages.ts | 958 ++++++++++++++++++ src/resources/messages/poll.ts | 151 +++ src/resources/webhook-events.ts | 9 + src/resources/webhooks.ts | 75 +- tests/api-resources/chats/chats.test.ts | 3 + tests/api-resources/chats/messages.test.ts | 3 + tests/api-resources/chats/polls.test.ts | 31 + .../{ => messages}/messages.test.ts | 3 + tests/api-resources/messages/poll.test.ts | 66 ++ 24 files changed, 1844 insertions(+), 1034 deletions(-) create mode 100644 src/resources/chats/polls.ts create mode 100644 src/resources/messages/index.ts create mode 100644 src/resources/messages/messages.ts create mode 100644 src/resources/messages/poll.ts create mode 100644 tests/api-resources/chats/polls.test.ts rename tests/api-resources/{ => messages}/messages.test.ts (98%) create mode 100644 tests/api-resources/messages/poll.test.ts diff --git a/.stats.yml b/.stats.yml index 03b0268..1c9b72d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1 +1 @@ -configured_endpoints: 57 +configured_endpoints: 61 diff --git a/api.md b/api.md index eacba93..5ae5d2f 100644 --- a/api.md +++ b/api.md @@ -79,26 +79,45 @@ Methods: - client.chats.location.retrieve(chatID) -> GetChatLocationResponse - client.chats.location.request(chatID) -> LocationRequestResponse +## Polls + +Types: + +- Poll +- PollEnvelope + +Methods: + +- client.chats.polls.create(chatID, { ...params }) -> PollEnvelope + # Messages Types: -- Message -- MessageEffect -- ReplyTo -- MessageCreateResponse -- MessageAddReactionResponse -- MessageUpdateAppCardResponse +- Message +- MessageEffect +- ReplyTo +- MessageCreateResponse +- MessageAddReactionResponse +- MessageUpdateAppCardResponse + +Methods: + +- client.messages.create({ ...params }) -> MessageCreateResponse +- client.messages.retrieve(messageID) -> Message +- client.messages.update(messageID, { ...params }) -> Message +- client.messages.delete(messageID) -> void +- client.messages.addReaction(messageID, { ...params }) -> MessageAddReactionResponse +- client.messages.listMessagesThread(messageID, { ...params }) -> MessagesListMessagesPagination +- client.messages.updateAppCard(messageID, { ...params }) -> MessageUpdateAppCardResponse + +## Poll Methods: -- client.messages.create({ ...params }) -> MessageCreateResponse -- client.messages.retrieve(messageID) -> Message -- client.messages.update(messageID, { ...params }) -> Message -- client.messages.delete(messageID) -> void -- client.messages.addReaction(messageID, { ...params }) -> MessageAddReactionResponse -- client.messages.listMessagesThread(messageID, { ...params }) -> MessagesListMessagesPagination -- client.messages.updateAppCard(messageID, { ...params }) -> MessageUpdateAppCardResponse +- client.messages.poll.retrieve(messageID) -> PollEnvelope +- client.messages.poll.addOptions(messageID, { ...params }) -> PollEnvelope +- client.messages.poll.vote(messageID, { ...params }) -> PollEnvelope # Attachments diff --git a/packages/mcp-server/src/code-tool-worker.ts b/packages/mcp-server/src/code-tool-worker.ts index 231eece..f23448f 100644 --- a/packages/mcp-server/src/code-tool-worker.ts +++ b/packages/mcp-server/src/code-tool-worker.ts @@ -124,6 +124,7 @@ const fuse = new Fuse( 'client.chats.messages.send', 'client.chats.location.request', 'client.chats.location.retrieve', + 'client.chats.polls.create', 'client.messages.addReaction', 'client.messages.create', 'client.messages.delete', @@ -131,6 +132,9 @@ const fuse = new Fuse( 'client.messages.retrieve', 'client.messages.update', 'client.messages.updateAppCard', + 'client.messages.poll.addOptions', + 'client.messages.poll.retrieve', + 'client.messages.poll.vote', 'client.attachments.create', 'client.attachments.delete', 'client.attachments.retrieve', diff --git a/packages/mcp-server/src/local-docs-search.ts b/packages/mcp-server/src/local-docs-search.ts index 4e86977..262ecf1 100644 --- a/packages/mcp-server/src/local-docs-search.ts +++ b/packages/mcp-server/src/local-docs-search.ts @@ -61,13 +61,14 @@ const EMBEDDED_METHODS: MethodEntry[] = [ qualified: 'client.chats.create', params: [ 'from: string;', - "message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; };", + "message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; mention?: string; mention_range?: number[]; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; };", 'to: string[];', + 'override_optout?: boolean;', ], response: "{ chat: { id: string; display_name: string; handles: object[]; health_status: { doc_url: string; status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; updated_at: string; }; is_group: boolean; message: object; service: 'iMessage' | 'SMS' | 'RCS'; }; }", markdown: - "## create\n\n`client.chats.create(from: string, message: { action?: object; effect?: message_effect; idempotency_key?: string; parts?: text_part | media_part | link_part | object[]; preferred_service?: service_type; reply_to?: reply_to; }, to: string[]): { chat: object; }`\n\n**post** `/v3/chats`\n\nCreate a new chat with specified participants and send an initial message.\nThe initial message is required when creating a chat.\n\n## Message Effects\n\nYou can add iMessage effects to make your messages more expressive. Effects are\noptional and can be either screen effects (full-screen animations) or bubble effects\n(message bubble animations).\n\n**Screen Effects:** `confetti`, `fireworks`, `lasers`, `sparkles`, `celebration`,\n`hearts`, `love`, `balloons`, `happy_birthday`, `echo`, `spotlight`\n\n**Bubble Effects:** `slam`, `loud`, `gentle`, `invisible`\n\nOnly one effect type can be applied per message.\n\n## Inline Text Decorations (iMessage only)\n\nUse the `text_decorations` array on a text part to apply styling and animations to character ranges.\n\nEach decoration specifies a `range: [start, end)` and exactly one of `style` or `animation`.\n\n**Styles:** `bold`, `italic`, `strikethrough`, `underline`\n**Animations:** `big`, `small`, `shake`, `nod`, `explode`, `ripple`, `bloom`, `jitter`\n\n```json\n{\n \"type\": \"text\",\n \"value\": \"Hello world\",\n \"text_decorations\": [\n { \"range\": [0, 5], \"style\": \"bold\" },\n { \"range\": [6, 11], \"animation\": \"shake\" }\n ]\n}\n```\n\n**Note:** Style ranges (bold, italic, etc.) may overlap, but animation ranges must not overlap with other animations or styles. Text decorations only render for iMessage recipients.\nFor SMS/RCS, text decorations are not applied.\n\n## First-Message Link Restriction\n\nTo protect sender deliverability, the **first outbound message** of a new chat cannot be a link.\nThe request is rejected with `400` (error code `1005`) when:\n\n- The message contains a `link` part (explicit rich-preview link), or\n- Any `text` part contains a URL.\n\nThis rule applies only to `POST /v3/chats`. Follow-up messages on an existing chat\n(`POST /v3/chats/{chatId}/messages`) are not subject to this restriction.\n\n## Reusing an Existing Chat\n\nChats are keyed on the `from` line plus the exact set of `to` handles. Repeating this\nrequest with the same `from` and `to` returns the **existing** chat and sends the message\ninto it instead of starting a second conversation.\n\nA group chat that has a `display_name` is excluded from that matching. To run several\nparallel groups over the same participants, name each one with `PUT /v3/chats/{chatId}`\nbefore creating the next: the following `POST /v3/chats` with the same `to` then returns a\nnew, separate `chat_id`. Two other cases also produce a new chat instead of reusing one —\nthe participant set changed (a participant was added or removed), or the `from` line left\nthe group.\n\nWhenever the response is a new chat, the first-message rules above apply to that request:\nno link in the first message, and no `reply_to` or message effect. To send into a chat you\nalready know, use `POST /v3/chats/{chatId}/messages` with its `chat_id`.\n\n\n### Parameters\n\n- `from: string`\n Sender phone number in E.164 format. Must be a phone number that the\nauthenticated partner has permission to send from.\n\n\n- `message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; }`\n Message content container. Groups all message-related fields together,\nseparating the \"what\" (message content) from the \"where\" (routing fields like from/to).\n\nA message carries EITHER `parts` — text and attachments, which compose\ninto one bubble — or a single `action`, which invokes an experience\ninside Linq's iMessage app. Never both: an app card is the whole message\n(Apple's `MSMessage` cannot coexist with text), so copy and a card are\ntwo sends, not one.\n\n - `action?: { action: string; experience: string; params?: object; }`\n Invokes an action on an experience — a third party that renders inside\nLinq's iMessage app. Linq resolves the recipient's connection, mints any\nsession the action needs, composes the card and sends it; none of that\nis visible to you.\n\nCall `GET /v3/experiences/{experience}` for the actions you may invoke\nand the fields each accepts.\n\n - `effect?: { name?: string; type?: 'screen' | 'bubble'; }`\n iMessage effect to apply to this message (screen or bubble effect)\n - `idempotency_key?: string`\n Optional idempotency key for this message.\nUse this to prevent duplicate sends of the same message. Reusing a key\nwhose message was deleted — or was an ephemeral message that has since\nexpired — returns 404; the message is never resent.\n\n - `parts?: { type: 'text'; value: string; text_decorations?: { range: number[]; animation?: 'big' | 'small' | 'shake' | 'nod' | 'explode' | 'ripple' | 'bloom' | 'jitter'; style?: 'bold' | 'italic' | 'strikethrough' | 'underline'; }[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]`\n Array of message parts. Each part can be text, media, or link.\nParts are displayed in order. Text and media can be mixed freely,\nbut a `link` part must be the only part in the message.\n\n**Rich Link Previews:**\n- Use a `link` part to send a URL with a rich preview card\n- A `link` part must be the **only** part in the message\n- To send a URL as plain text (no preview), use a `text` part instead\n\n**Supported Media:**\n- Images: .jpg, .jpeg, .png, .gif, .heic, .heif, .tif, .tiff, .bmp\n- Videos: .mp4, .mov, .m4v, .mpeg, .mpg, .3gp\n- Audio: .m4a, .mp3, .aac, .caf, .wav, .aiff, .amr\n- Documents: .pdf, .txt, .rtf, .csv, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pages, .numbers, .key, .epub, .zip, .html, .htm\n- Contact & Calendar: .vcf, .ics\n\n**Audio:**\n- Audio files (.m4a, .mp3, .aac, .caf, .wav, .aiff, .amr) are fully supported as media parts\n- To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated\n `/v3/chats/{chatId}/voicememo` endpoint instead\n\n**Validation Rules:**\n- A `link` part must be the **only** part in the message. It cannot be combined\n with text or media parts.\n- Consecutive text parts are not allowed. Text parts must be separated by\n media parts. For example, [text, text] is invalid, but [text, media, text] is valid.\n- Maximum of **100 parts** total.\n- Media parts using a public `url` (downloaded by the server on send) are\n capped at **40**. Parts using `attachment_id` or presigned URLs\n are exempt from this sub-limit. For bulk media sends exceeding 40 files,\n pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`.\n\n - `preferred_service?: 'iMessage' | 'SMS' | 'RCS'`\n Messaging service type\n - `reply_to?: { message_id: string; part_index?: number; }`\n Reply to another message to create a threaded conversation\n\n- `to: string[]`\n Array of recipient handles (phone numbers in E.164 format or email addresses).\nFor individual chats, provide one recipient. For group chats, provide multiple.\n\n\n### Returns\n\n- `{ chat: { id: string; display_name: string; handles: object[]; health_status: { doc_url: string; status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; updated_at: string; }; is_group: boolean; message: object; service: 'iMessage' | 'SMS' | 'RCS'; }; }`\n Response for creating a new chat with an initial message\n\n - `chat: { id: string; display_name: string; handles: { id: string; handle: string; joined_at: string; service: 'iMessage' | 'SMS' | 'RCS'; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }[]; health_status: { doc_url: string; status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; updated_at: string; }; is_group: boolean; message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: object | object | object | { app: object; layout: object; reactions: reaction[]; type: 'imessage_app'; url: string; fallback_text?: string; }[]; sent_at: string; delivered_at?: string; effect?: object; from_handle?: object; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: object; service?: 'iMessage' | 'SMS' | 'RCS'; }; service: 'iMessage' | 'SMS' | 'RCS'; }`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst chat = await client.chats.create({\n from: '+12052535597',\n message: {},\n to: ['+12052532136'],\n});\n\nconsole.log(chat);\n```", + "## create\n\n`client.chats.create(from: string, message: { action?: object; effect?: message_effect; idempotency_key?: string; parts?: text_part | media_part | link_part | object[]; preferred_service?: service_type; reply_to?: reply_to; }, to: string[], override_optout?: boolean): { chat: object; }`\n\n**post** `/v3/chats`\n\nCreate a new chat with specified participants and send an initial message.\nThe initial message is required when creating a chat.\n\n## Message Effects\n\nYou can add iMessage effects to make your messages more expressive. Effects are\noptional and can be either screen effects (full-screen animations) or bubble effects\n(message bubble animations).\n\n**Screen Effects:** `confetti`, `fireworks`, `lasers`, `sparkles`, `celebration`,\n`hearts`, `love`, `balloons`, `happy_birthday`, `echo`, `spotlight`\n\n**Bubble Effects:** `slam`, `loud`, `gentle`, `invisible`\n\nOnly one effect type can be applied per message.\n\n## Inline Text Decorations (iMessage only)\n\nUse the `text_decorations` array on a text part to apply styling and animations to character ranges.\n\nEach decoration specifies a `range: [start, end)` and exactly one of `style` or `animation`.\n\n**Styles:** `bold`, `italic`, `strikethrough`, `underline`\n**Animations:** `big`, `small`, `shake`, `nod`, `explode`, `ripple`, `bloom`, `jitter`\n\n```json\n{\n \"type\": \"text\",\n \"value\": \"Hello world\",\n \"text_decorations\": [\n { \"range\": [0, 5], \"style\": \"bold\" },\n { \"range\": [6, 11], \"animation\": \"shake\" }\n ]\n}\n```\n\n**Note:** Style ranges (bold, italic, etc.) may overlap, but animation ranges must not overlap with other animations or styles. Text decorations only render for iMessage recipients.\nFor SMS/RCS, text decorations are not applied.\n\n## First-Message Link Restriction\n\nTo protect sender deliverability, the **first outbound message** of a new chat cannot be a link.\nThe request is rejected with `400` (error code `1005`) when:\n\n- The message contains a `link` part (explicit rich-preview link), or\n- Any `text` part contains a URL.\n\nThis rule applies only to `POST /v3/chats`. Follow-up messages on an existing chat\n(`POST /v3/chats/{chatId}/messages`) are not subject to this restriction.\n\n## Reusing an Existing Chat\n\nChats are keyed on the `from` line plus the exact set of `to` handles. Repeating this\nrequest with the same `from` and `to` returns the **existing** chat and sends the message\ninto it instead of starting a second conversation.\n\nA group chat that has a `display_name` is excluded from that matching. To run several\nparallel groups over the same participants, name each one with `PUT /v3/chats/{chatId}`\nbefore creating the next: the following `POST /v3/chats` with the same `to` then returns a\nnew, separate `chat_id`. Two other cases also produce a new chat instead of reusing one —\nthe participant set changed (a participant was added or removed), or the `from` line left\nthe group.\n\nWhenever the response is a new chat, the first-message rules above apply to that request:\nno link in the first message, and no `reply_to` or message effect. To send into a chat you\nalready know, use `POST /v3/chats/{chatId}/messages` with its `chat_id`.\n\n\n### Parameters\n\n- `from: string`\n Sender phone number in E.164 format. Must be a phone number that the\nauthenticated partner has permission to send from.\n\n\n- `message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; mention?: string; mention_range?: number[]; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; }`\n Message content container. Groups all message-related fields together,\nseparating the \"what\" (message content) from the \"where\" (routing fields like from/to).\n\nA message carries EITHER `parts` — text and attachments, which compose\ninto one bubble — or a single `action`, which invokes an experience\ninside Linq's iMessage app. Never both: an app card is the whole message\n(Apple's `MSMessage` cannot coexist with text), so copy and a card are\ntwo sends, not one.\n\n - `action?: { action: string; experience: string; params?: object; }`\n Invokes an action on an experience — a third party that renders inside\nLinq's iMessage app. Linq resolves the recipient's connection, mints any\nsession the action needs, composes the card and sends it; none of that\nis visible to you.\n\nCall `GET /v3/experiences/{experience}` for the actions you may invoke\nand the fields each accepts.\n\n - `effect?: { name?: string; type?: 'screen' | 'bubble'; }`\n iMessage effect to apply to this message (screen or bubble effect)\n - `idempotency_key?: string`\n Optional idempotency key for this message.\nUse this to prevent duplicate sends of the same message. Reusing a key\nwhose message was deleted — or was an ephemeral message that has since\nexpired — returns 404; the message is never resent.\n\n - `parts?: { type: 'text'; value: string; mention?: string; mention_range?: number[]; text_decorations?: { range: number[]; animation?: 'big' | 'small' | 'shake' | 'nod' | 'explode' | 'ripple' | 'bloom' | 'jitter'; style?: 'bold' | 'italic' | 'strikethrough' | 'underline'; }[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]`\n Array of message parts. Each part can be text, media, or link.\nParts are displayed in order. Text and media can be mixed freely,\nbut a `link` part must be the only part in the message.\n\n**Rich Link Previews:**\n- Use a `link` part to send a URL with a rich preview card\n- A `link` part must be the **only** part in the message\n- To send a URL as plain text (no preview), use a `text` part instead\n\n**Supported Media:**\n- Images: .jpg, .jpeg, .png, .gif, .heic, .heif, .tif, .tiff, .bmp\n- Videos: .mp4, .mov, .m4v, .mpeg, .mpg, .3gp\n- Audio: .m4a, .mp3, .aac, .caf, .wav, .aiff, .amr\n- Documents: .pdf, .txt, .rtf, .csv, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pages, .numbers, .key, .epub, .zip, .html, .htm\n- Contact & Calendar: .vcf, .ics\n\n**Audio:**\n- Audio files (.m4a, .mp3, .aac, .caf, .wav, .aiff, .amr) are fully supported as media parts\n- To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated\n `/v3/chats/{chatId}/voicememo` endpoint instead\n\n**Validation Rules:**\n- A `link` part must be the **only** part in the message. It cannot be combined\n with text or media parts.\n- Consecutive text parts are not allowed. Text parts must be separated by\n media parts. For example, [text, text] is invalid, but [text, media, text] is valid.\n- Maximum of **100 parts** total.\n- Media parts using a public `url` (downloaded by the server on send) are\n capped at **40**. Parts using `attachment_id` or presigned URLs\n are exempt from this sub-limit. For bulk media sends exceeding 40 files,\n pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`.\n\n - `preferred_service?: 'iMessage' | 'SMS' | 'RCS'`\n Messaging service type\n - `reply_to?: { message_id: string; part_index?: number; }`\n Reply to another message to create a threaded conversation\n\n- `to: string[]`\n Array of recipient handles (phone numbers in E.164 format or email addresses).\nFor individual chats, provide one recipient. For group chats, provide multiple.\n\n\n- `override_optout?: boolean`\n Send even though the recipient asked you to stop (`403`, error code\n`2024`). Applies to this request only: the opt-out stays in place, so\nthe next send without this flag is rejected again. Every override is\nrecorded against your API key.\n\n\n### Returns\n\n- `{ chat: { id: string; display_name: string; handles: object[]; health_status: { doc_url: string; status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; updated_at: string; }; is_group: boolean; message: object; service: 'iMessage' | 'SMS' | 'RCS'; }; }`\n Response for creating a new chat with an initial message\n\n - `chat: { id: string; display_name: string; handles: { id: string; handle: string; joined_at: string; service: 'iMessage' | 'SMS' | 'RCS'; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }[]; health_status: { doc_url: string; status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; updated_at: string; }; is_group: boolean; message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: object | object | object | { app: object; layout: object; reactions: reaction[]; type: 'imessage_app'; url: string; fallback_text?: string; }[]; sent_at: string; delivered_at?: string; effect?: object; from_handle?: object; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: object; service?: 'iMessage' | 'SMS' | 'RCS'; }; service: 'iMessage' | 'SMS' | 'RCS'; }`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst chat = await client.chats.create({\n from: '+12052535597',\n message: {},\n to: ['+12052532136'],\n});\n\nconsole.log(chat);\n```", perLanguage: { go: { method: 'client.Chats.New', @@ -86,7 +87,7 @@ const EMBEDDED_METHODS: MethodEntry[] = [ }, http: { example: - 'curl https://api.linqapp.com/api/partner/v3/chats \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "from": "+12052535597",\n "message": {\n "parts": [\n {\n "type": "text",\n "value": "Hello! How can I help you today?"\n }\n ]\n },\n "to": [\n "+12052532136"\n ]\n }\'', + 'curl https://api.linqapp.com/api/partner/v3/chats \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "from": "+12052535597",\n "message": {\n "parts": [\n {\n "type": "text",\n "value": "Hello! How can I help you today?"\n }\n ]\n },\n "to": [\n "+12052532136"\n ],\n "override_optout": false\n }\'', }, }, }, @@ -307,11 +308,16 @@ const EMBEDDED_METHODS: MethodEntry[] = [ "Send an audio file as an **iMessage voice memo bubble** to all participants in a chat.\nVoice memos appear with iMessage's native inline playback UI, unlike regular audio\nattachments sent via media parts which appear as downloadable files.\n\n**Supported audio formats:**\n- MP3 (audio/mpeg)\n- M4A (audio/x-m4a, audio/mp4)\n- AAC (audio/aac)\n- CAF (audio/x-caf) - Core Audio Format\n- WAV (audio/wav)\n- AIFF (audio/aiff, audio/x-aiff)\n- AMR (audio/amr)\n", stainlessPath: '(resource) chats > (method) send_voicememo', qualified: 'client.chats.sendVoicememo', - params: ['chatId: string;', 'attachment_id?: string;', 'voice_memo_url?: string;'], + params: [ + 'chatId: string;', + 'attachment_id?: string;', + 'override_optout?: boolean;', + 'voice_memo_url?: string;', + ], response: "{ voice_memo: { id: string; chat: { id: string; handles: chat_handle[]; is_active: boolean; is_group: boolean; service: service_type; }; created_at: string; from: string; status: string; to: string[]; voice_memo: { id: string; filename: string; mime_type: string; size_bytes: number; url: string; duration_ms?: number; }; service?: 'iMessage' | 'SMS' | 'RCS'; }; }", markdown: - "## send_voicememo\n\n`client.chats.sendVoicememo(chatId: string, attachment_id?: string, voice_memo_url?: string): { voice_memo: object; }`\n\n**post** `/v3/chats/{chatId}/voicememo`\n\nSend an audio file as an **iMessage voice memo bubble** to all participants in a chat.\nVoice memos appear with iMessage's native inline playback UI, unlike regular audio\nattachments sent via media parts which appear as downloadable files.\n\n**Supported audio formats:**\n- MP3 (audio/mpeg)\n- M4A (audio/x-m4a, audio/mp4)\n- AAC (audio/aac)\n- CAF (audio/x-caf) - Core Audio Format\n- WAV (audio/wav)\n- AIFF (audio/aiff, audio/x-aiff)\n- AMR (audio/amr)\n\n\n### Parameters\n\n- `chatId: string`\n\n- `attachment_id?: string`\n Reference to a voice memo file pre-uploaded via `POST /v3/attachments`.\nThe file is already stored, so sends using this ID skip the download step.\n\nEither `voice_memo_url` or `attachment_id` must be provided, but not both.\n\n\n- `voice_memo_url?: string`\n URL of the voice memo audio file. Must be a publicly accessible HTTPS URL.\n\nEither `voice_memo_url` or `attachment_id` must be provided, but not both.\n\n\n### Returns\n\n- `{ voice_memo: { id: string; chat: { id: string; handles: chat_handle[]; is_active: boolean; is_group: boolean; service: service_type; }; created_at: string; from: string; status: string; to: string[]; voice_memo: { id: string; filename: string; mime_type: string; size_bytes: number; url: string; duration_ms?: number; }; service?: 'iMessage' | 'SMS' | 'RCS'; }; }`\n Response for sending a voice memo to a chat\n\n - `voice_memo: { id: string; chat: { id: string; handles: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }[]; is_active: boolean; is_group: boolean; service: 'iMessage' | 'SMS' | 'RCS'; }; created_at: string; from: string; status: string; to: string[]; voice_memo: { id: string; filename: string; mime_type: string; size_bytes: number; url: string; duration_ms?: number; }; service?: 'iMessage' | 'SMS' | 'RCS'; }`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst response = await client.chats.sendVoicememo('f19ee7b8-8533-4c5c-83ec-4ef8d6d1ddbd');\n\nconsole.log(response);\n```", + "## send_voicememo\n\n`client.chats.sendVoicememo(chatId: string, attachment_id?: string, override_optout?: boolean, voice_memo_url?: string): { voice_memo: object; }`\n\n**post** `/v3/chats/{chatId}/voicememo`\n\nSend an audio file as an **iMessage voice memo bubble** to all participants in a chat.\nVoice memos appear with iMessage's native inline playback UI, unlike regular audio\nattachments sent via media parts which appear as downloadable files.\n\n**Supported audio formats:**\n- MP3 (audio/mpeg)\n- M4A (audio/x-m4a, audio/mp4)\n- AAC (audio/aac)\n- CAF (audio/x-caf) - Core Audio Format\n- WAV (audio/wav)\n- AIFF (audio/aiff, audio/x-aiff)\n- AMR (audio/amr)\n\n\n### Parameters\n\n- `chatId: string`\n\n- `attachment_id?: string`\n Reference to a voice memo file pre-uploaded via `POST /v3/attachments`.\nThe file is already stored, so sends using this ID skip the download step.\n\nEither `voice_memo_url` or `attachment_id` must be provided, but not both.\n\n\n- `override_optout?: boolean`\n Send even though the recipient asked you to stop (`403`, error code\n`2024`). Applies to this request only: the opt-out stays in place, so\nthe next send without this flag is rejected again. Every override is\nrecorded against your API key.\n\n\n- `voice_memo_url?: string`\n URL of the voice memo audio file. Must be a publicly accessible HTTPS URL.\n\nEither `voice_memo_url` or `attachment_id` must be provided, but not both.\n\n\n### Returns\n\n- `{ voice_memo: { id: string; chat: { id: string; handles: chat_handle[]; is_active: boolean; is_group: boolean; service: service_type; }; created_at: string; from: string; status: string; to: string[]; voice_memo: { id: string; filename: string; mime_type: string; size_bytes: number; url: string; duration_ms?: number; }; service?: 'iMessage' | 'SMS' | 'RCS'; }; }`\n Response for sending a voice memo to a chat\n\n - `voice_memo: { id: string; chat: { id: string; handles: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }[]; is_active: boolean; is_group: boolean; service: 'iMessage' | 'SMS' | 'RCS'; }; created_at: string; from: string; status: string; to: string[]; voice_memo: { id: string; filename: string; mime_type: string; size_bytes: number; url: string; duration_ms?: number; }; service?: 'iMessage' | 'SMS' | 'RCS'; }`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst response = await client.chats.sendVoicememo('f19ee7b8-8533-4c5c-83ec-4ef8d6d1ddbd');\n\nconsole.log(response);\n```", perLanguage: { go: { method: 'client.Chats.SendVoicememo', @@ -330,7 +336,7 @@ const EMBEDDED_METHODS: MethodEntry[] = [ }, http: { example: - 'curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/voicememo \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "voice_memo_url": "https://example.com/voice-memo.m4a"\n }\'', + 'curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/voicememo \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "override_optout": false,\n "voice_memo_url": "https://example.com/voice-memo.m4a"\n }\'', }, }, }, @@ -483,12 +489,13 @@ const EMBEDDED_METHODS: MethodEntry[] = [ qualified: 'client.chats.messages.send', params: [ 'chatId: string;', - "message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; };", + "message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; mention?: string; mention_range?: number[]; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; };", + 'override_optout?: boolean;', ], response: "{ chat_id: string; message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: text_part_response | media_part_response | link_part_response | object[]; sent_at: string; delivered_at?: string; effect?: message_effect; from_handle?: chat_handle; preferred_service?: service_type; reply_to?: reply_to; service?: service_type; }; }", markdown: - "## send\n\n`client.chats.messages.send(chatId: string, message: { action?: object; effect?: message_effect; idempotency_key?: string; parts?: text_part | media_part | link_part | object[]; preferred_service?: service_type; reply_to?: reply_to; }): { chat_id: string; message: sent_message; }`\n\n**post** `/v3/chats/{chatId}/messages`\n\nSend a message to an existing chat. Use this endpoint when you already have\na chat ID and want to send additional messages to it.\n\n## Message Effects\n\nYou can add iMessage effects to make your messages more expressive. Effects are\noptional and can be either screen effects (full-screen animations) or bubble effects\n(message bubble animations).\n\n**Screen Effects:** `confetti`, `fireworks`, `lasers`, `sparkles`, `celebration`,\n`hearts`, `love`, `balloons`, `happy_birthday`, `echo`, `spotlight`\n\n**Bubble Effects:** `slam`, `loud`, `gentle`, `invisible`\n\nOnly one effect type can be applied per message.\n\n## Inline Text Decorations (iMessage only)\n\nUse the `text_decorations` array on a text part to apply styling and animations to character ranges.\n\nEach decoration specifies a `range: [start, end)` and exactly one of `style` or `animation`.\n\n**Styles:** `bold`, `italic`, `strikethrough`, `underline`\n**Animations:** `big`, `small`, `shake`, `nod`, `explode`, `ripple`, `bloom`, `jitter`\n\n```json\n{\n \"type\": \"text\",\n \"value\": \"Hello world\",\n \"text_decorations\": [\n { \"range\": [0, 5], \"style\": \"bold\" },\n { \"range\": [6, 11], \"animation\": \"shake\" }\n ]\n}\n```\n\n**Note:** Style ranges (bold, italic, etc.) may overlap, but animation ranges must not overlap with other animations or styles. Text decorations only render for iMessage recipients.\nFor SMS/RCS, text decorations are not applied.\n\n\n### Parameters\n\n- `chatId: string`\n\n- `message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; }`\n Message content container. Groups all message-related fields together,\nseparating the \"what\" (message content) from the \"where\" (routing fields like from/to).\n\nA message carries EITHER `parts` — text and attachments, which compose\ninto one bubble — or a single `action`, which invokes an experience\ninside Linq's iMessage app. Never both: an app card is the whole message\n(Apple's `MSMessage` cannot coexist with text), so copy and a card are\ntwo sends, not one.\n\n - `action?: { action: string; experience: string; params?: object; }`\n Invokes an action on an experience — a third party that renders inside\nLinq's iMessage app. Linq resolves the recipient's connection, mints any\nsession the action needs, composes the card and sends it; none of that\nis visible to you.\n\nCall `GET /v3/experiences/{experience}` for the actions you may invoke\nand the fields each accepts.\n\n - `effect?: { name?: string; type?: 'screen' | 'bubble'; }`\n iMessage effect to apply to this message (screen or bubble effect)\n - `idempotency_key?: string`\n Optional idempotency key for this message.\nUse this to prevent duplicate sends of the same message. Reusing a key\nwhose message was deleted — or was an ephemeral message that has since\nexpired — returns 404; the message is never resent.\n\n - `parts?: { type: 'text'; value: string; text_decorations?: { range: number[]; animation?: 'big' | 'small' | 'shake' | 'nod' | 'explode' | 'ripple' | 'bloom' | 'jitter'; style?: 'bold' | 'italic' | 'strikethrough' | 'underline'; }[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]`\n Array of message parts. Each part can be text, media, or link.\nParts are displayed in order. Text and media can be mixed freely,\nbut a `link` part must be the only part in the message.\n\n**Rich Link Previews:**\n- Use a `link` part to send a URL with a rich preview card\n- A `link` part must be the **only** part in the message\n- To send a URL as plain text (no preview), use a `text` part instead\n\n**Supported Media:**\n- Images: .jpg, .jpeg, .png, .gif, .heic, .heif, .tif, .tiff, .bmp\n- Videos: .mp4, .mov, .m4v, .mpeg, .mpg, .3gp\n- Audio: .m4a, .mp3, .aac, .caf, .wav, .aiff, .amr\n- Documents: .pdf, .txt, .rtf, .csv, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pages, .numbers, .key, .epub, .zip, .html, .htm\n- Contact & Calendar: .vcf, .ics\n\n**Audio:**\n- Audio files (.m4a, .mp3, .aac, .caf, .wav, .aiff, .amr) are fully supported as media parts\n- To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated\n `/v3/chats/{chatId}/voicememo` endpoint instead\n\n**Validation Rules:**\n- A `link` part must be the **only** part in the message. It cannot be combined\n with text or media parts.\n- Consecutive text parts are not allowed. Text parts must be separated by\n media parts. For example, [text, text] is invalid, but [text, media, text] is valid.\n- Maximum of **100 parts** total.\n- Media parts using a public `url` (downloaded by the server on send) are\n capped at **40**. Parts using `attachment_id` or presigned URLs\n are exempt from this sub-limit. For bulk media sends exceeding 40 files,\n pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`.\n\n - `preferred_service?: 'iMessage' | 'SMS' | 'RCS'`\n Messaging service type\n - `reply_to?: { message_id: string; part_index?: number; }`\n Reply to another message to create a threaded conversation\n\n### Returns\n\n- `{ chat_id: string; message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: text_part_response | media_part_response | link_part_response | object[]; sent_at: string; delivered_at?: string; effect?: message_effect; from_handle?: chat_handle; preferred_service?: service_type; reply_to?: reply_to; service?: service_type; }; }`\n Response for sending a message to a chat\n\n - `chat_id: string`\n - `message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: { reactions: reaction[]; type: 'text'; value: string; text_decorations?: text_decoration[]; } | { id: string; filename: string; mime_type: string; reactions: reaction[]; size_bytes: number; type: 'media'; url: string; } | { reactions: reaction[]; type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; reactions: object[]; type: 'imessage_app'; url: string; fallback_text?: string; }[]; sent_at: string; delivered_at?: string; effect?: { name?: string; type?: 'screen' | 'bubble'; }; from_handle?: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; service?: 'iMessage' | 'SMS' | 'RCS'; }`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst response = await client.chats.messages.send('550e8400-e29b-41d4-a716-446655440000', { message: {} });\n\nconsole.log(response);\n```", + "## send\n\n`client.chats.messages.send(chatId: string, message: { action?: object; effect?: message_effect; idempotency_key?: string; parts?: text_part | media_part | link_part | object[]; preferred_service?: service_type; reply_to?: reply_to; }, override_optout?: boolean): { chat_id: string; message: sent_message; }`\n\n**post** `/v3/chats/{chatId}/messages`\n\nSend a message to an existing chat. Use this endpoint when you already have\na chat ID and want to send additional messages to it.\n\n## Message Effects\n\nYou can add iMessage effects to make your messages more expressive. Effects are\noptional and can be either screen effects (full-screen animations) or bubble effects\n(message bubble animations).\n\n**Screen Effects:** `confetti`, `fireworks`, `lasers`, `sparkles`, `celebration`,\n`hearts`, `love`, `balloons`, `happy_birthday`, `echo`, `spotlight`\n\n**Bubble Effects:** `slam`, `loud`, `gentle`, `invisible`\n\nOnly one effect type can be applied per message.\n\n## Inline Text Decorations (iMessage only)\n\nUse the `text_decorations` array on a text part to apply styling and animations to character ranges.\n\nEach decoration specifies a `range: [start, end)` and exactly one of `style` or `animation`.\n\n**Styles:** `bold`, `italic`, `strikethrough`, `underline`\n**Animations:** `big`, `small`, `shake`, `nod`, `explode`, `ripple`, `bloom`, `jitter`\n\n```json\n{\n \"type\": \"text\",\n \"value\": \"Hello world\",\n \"text_decorations\": [\n { \"range\": [0, 5], \"style\": \"bold\" },\n { \"range\": [6, 11], \"animation\": \"shake\" }\n ]\n}\n```\n\n**Note:** Style ranges (bold, italic, etc.) may overlap, but animation ranges must not overlap with other animations or styles. Text decorations only render for iMessage recipients.\nFor SMS/RCS, text decorations are not applied.\n\n\n### Parameters\n\n- `chatId: string`\n\n- `message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; mention?: string; mention_range?: number[]; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; }`\n Message content container. Groups all message-related fields together,\nseparating the \"what\" (message content) from the \"where\" (routing fields like from/to).\n\nA message carries EITHER `parts` — text and attachments, which compose\ninto one bubble — or a single `action`, which invokes an experience\ninside Linq's iMessage app. Never both: an app card is the whole message\n(Apple's `MSMessage` cannot coexist with text), so copy and a card are\ntwo sends, not one.\n\n - `action?: { action: string; experience: string; params?: object; }`\n Invokes an action on an experience — a third party that renders inside\nLinq's iMessage app. Linq resolves the recipient's connection, mints any\nsession the action needs, composes the card and sends it; none of that\nis visible to you.\n\nCall `GET /v3/experiences/{experience}` for the actions you may invoke\nand the fields each accepts.\n\n - `effect?: { name?: string; type?: 'screen' | 'bubble'; }`\n iMessage effect to apply to this message (screen or bubble effect)\n - `idempotency_key?: string`\n Optional idempotency key for this message.\nUse this to prevent duplicate sends of the same message. Reusing a key\nwhose message was deleted — or was an ephemeral message that has since\nexpired — returns 404; the message is never resent.\n\n - `parts?: { type: 'text'; value: string; mention?: string; mention_range?: number[]; text_decorations?: { range: number[]; animation?: 'big' | 'small' | 'shake' | 'nod' | 'explode' | 'ripple' | 'bloom' | 'jitter'; style?: 'bold' | 'italic' | 'strikethrough' | 'underline'; }[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]`\n Array of message parts. Each part can be text, media, or link.\nParts are displayed in order. Text and media can be mixed freely,\nbut a `link` part must be the only part in the message.\n\n**Rich Link Previews:**\n- Use a `link` part to send a URL with a rich preview card\n- A `link` part must be the **only** part in the message\n- To send a URL as plain text (no preview), use a `text` part instead\n\n**Supported Media:**\n- Images: .jpg, .jpeg, .png, .gif, .heic, .heif, .tif, .tiff, .bmp\n- Videos: .mp4, .mov, .m4v, .mpeg, .mpg, .3gp\n- Audio: .m4a, .mp3, .aac, .caf, .wav, .aiff, .amr\n- Documents: .pdf, .txt, .rtf, .csv, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pages, .numbers, .key, .epub, .zip, .html, .htm\n- Contact & Calendar: .vcf, .ics\n\n**Audio:**\n- Audio files (.m4a, .mp3, .aac, .caf, .wav, .aiff, .amr) are fully supported as media parts\n- To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated\n `/v3/chats/{chatId}/voicememo` endpoint instead\n\n**Validation Rules:**\n- A `link` part must be the **only** part in the message. It cannot be combined\n with text or media parts.\n- Consecutive text parts are not allowed. Text parts must be separated by\n media parts. For example, [text, text] is invalid, but [text, media, text] is valid.\n- Maximum of **100 parts** total.\n- Media parts using a public `url` (downloaded by the server on send) are\n capped at **40**. Parts using `attachment_id` or presigned URLs\n are exempt from this sub-limit. For bulk media sends exceeding 40 files,\n pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`.\n\n - `preferred_service?: 'iMessage' | 'SMS' | 'RCS'`\n Messaging service type\n - `reply_to?: { message_id: string; part_index?: number; }`\n Reply to another message to create a threaded conversation\n\n- `override_optout?: boolean`\n Send even though the recipient asked you to stop (`403`, error code\n`2024`). Applies to this request only: the opt-out stays in place, so\nthe next send without this flag is rejected again. Every override is\nrecorded against your API key.\n\n\n### Returns\n\n- `{ chat_id: string; message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: text_part_response | media_part_response | link_part_response | object[]; sent_at: string; delivered_at?: string; effect?: message_effect; from_handle?: chat_handle; preferred_service?: service_type; reply_to?: reply_to; service?: service_type; }; }`\n Response for sending a message to a chat\n\n - `chat_id: string`\n - `message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: { reactions: reaction[]; type: 'text'; value: string; text_decorations?: text_decoration[]; } | { id: string; filename: string; mime_type: string; reactions: reaction[]; size_bytes: number; type: 'media'; url: string; } | { reactions: reaction[]; type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; reactions: object[]; type: 'imessage_app'; url: string; fallback_text?: string; }[]; sent_at: string; delivered_at?: string; effect?: { name?: string; type?: 'screen' | 'bubble'; }; from_handle?: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; service?: 'iMessage' | 'SMS' | 'RCS'; }`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst response = await client.chats.messages.send('550e8400-e29b-41d4-a716-446655440000', { message: {} });\n\nconsole.log(response);\n```", perLanguage: { go: { method: 'client.Chats.Messages.Send', @@ -507,7 +514,7 @@ const EMBEDDED_METHODS: MethodEntry[] = [ }, http: { example: - 'curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/messages \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "message": {\n "parts": [\n {\n "type": "text",\n "value": "Hello, world!"\n }\n ]\n }\n }\'', + 'curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/messages \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "message": {\n "parts": [\n {\n "type": "text",\n "value": "Hello, world!"\n }\n ]\n },\n "override_optout": false\n }\'', }, }, }, @@ -617,6 +624,42 @@ const EMBEDDED_METHODS: MethodEntry[] = [ }, }, }, + { + name: 'create', + endpoint: '/v3/chats/{chatId}/polls', + httpMethod: 'post', + summary: 'Create and send a poll in a chat', + description: + 'Create an iMessage poll in an existing chat and send it. Polls are iMessage-only.\n\nThe chat must already exist — **a poll cannot be the first message of a\nnew chat** (use `POST /v3/chats` for that). Options are **add-only and immutable**: you\ncan add options later via `POST /v3/messages/{messageId}/poll/options`, but never edit\nor remove them.\n', + stainlessPath: '(resource) chats.polls > (method) create', + qualified: 'client.chats.polls.create', + params: ['chatId: string;', 'poll: { options: { text: string; }[]; idempotency_key?: string; };'], + response: + '{ chat_id: string; created_at: string; message_id: string; poll: { options: object[]; total_voters: number; }; reactions: { handle: chat_handle; is_me: boolean; type: reaction_type; custom_emoji?: string; sticker?: object; }[]; updated_at: string; }', + markdown: + "## create\n\n`client.chats.polls.create(chatId: string, poll: { options: { text: string; }[]; idempotency_key?: string; }): { chat_id: string; created_at: string; message_id: string; poll: poll; reactions: reaction[]; updated_at: string; }`\n\n**post** `/v3/chats/{chatId}/polls`\n\nCreate an iMessage poll in an existing chat and send it. Polls are iMessage-only.\n\nThe chat must already exist — **a poll cannot be the first message of a\nnew chat** (use `POST /v3/chats` for that). Options are **add-only and immutable**: you\ncan add options later via `POST /v3/messages/{messageId}/poll/options`, but never edit\nor remove them.\n\n\n### Parameters\n\n- `chatId: string`\n\n- `poll: { options: { text: string; }[]; idempotency_key?: string; }`\n Poll content to create. A poll needs at least two options. Options are add-only and\nimmutable — there is no title/question (send that as a normal text message).\n\n - `options: { text: string; }[]`\n - `idempotency_key?: string`\n Optional key to deduplicate the poll creation.\n\n### Returns\n\n- `{ chat_id: string; created_at: string; message_id: string; poll: { options: object[]; total_voters: number; }; reactions: { handle: chat_handle; is_me: boolean; type: reaction_type; custom_emoji?: string; sticker?: object; }[]; updated_at: string; }`\n Message-level envelope returned by every poll endpoint.\n\n - `chat_id: string`\n - `created_at: string`\n - `message_id: string`\n - `poll: { options: { can_be_edited: boolean; creator_handle: object; option_id: string; text: string; voters: { handle: string; voted_at: string; }[]; }[]; total_voters: number; }`\n - `reactions: { handle: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }; is_me: boolean; type: 'love' | 'like' | 'dislike' | 'laugh' | 'emphasize' | 'question' | 'custom' | 'sticker'; custom_emoji?: string; sticker?: { file_name?: string; height?: number; mime_type?: string; url?: string; width?: number; }; }[]`\n - `updated_at: string`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst pollEnvelope = await client.chats.polls.create('550e8400-e29b-41d4-a716-446655440000', { poll: { options: [{ text: 'Tacos' }, { text: 'Sushi' }] } });\n\nconsole.log(pollEnvelope);\n```", + perLanguage: { + go: { + method: 'client.Chats.Polls.New', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/linq-team/linq-go"\n\t"github.com/linq-team/linq-go/option"\n)\n\nfunc main() {\n\tclient := linqgo.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpollEnvelope, err := client.Chats.Polls.New(\n\t\tcontext.TODO(),\n\t\t"550e8400-e29b-41d4-a716-446655440000",\n\t\tlinqgo.ChatPollNewParams{\n\t\t\tPoll: linqgo.ChatPollNewParamsPoll{\n\t\t\t\tOptions: []linqgo.ChatPollNewParamsPollOption{{\n\t\t\t\t\tText: "Tacos",\n\t\t\t\t}, {\n\t\t\t\t\tText: "Sushi",\n\t\t\t\t}},\n\t\t\t\tIdempotencyKey: linqgo.String("poll-abc123"),\n\t\t\t},\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", pollEnvelope.ChatID)\n}\n', + }, + python: { + method: 'chats.polls.create', + example: + 'import os\nfrom linq import LinqAPIV3\n\nclient = LinqAPIV3(\n api_key=os.environ.get("LINQ_API_V3_API_KEY"), # This is the default and can be omitted\n)\npoll_envelope = client.chats.polls.create(\n chat_id="550e8400-e29b-41d4-a716-446655440000",\n poll={\n "options": [{\n "text": "Tacos"\n }, {\n "text": "Sushi"\n }],\n "idempotency_key": "poll-abc123",\n },\n)\nprint(poll_envelope.chat_id)', + }, + typescript: { + method: 'client.chats.polls.create', + example: + "import LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3({\n apiKey: process.env['LINQ_API_V3_API_KEY'], // This is the default and can be omitted\n});\n\nconst pollEnvelope = await client.chats.polls.create('550e8400-e29b-41d4-a716-446655440000', {\n poll: { options: [{ text: 'Tacos' }, { text: 'Sushi' }], idempotency_key: 'poll-abc123' },\n});\n\nconsole.log(pollEnvelope.chat_id);", + }, + http: { + example: + 'curl https://api.linqapp.com/api/partner/v3/chats/$CHAT_ID/polls \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "poll": {\n "options": [\n {\n "text": "Tacos"\n },\n {\n "text": "Sushi"\n }\n ],\n "idempotency_key": "poll-abc123"\n }\n }\'', + }, + }, + }, { name: 'create', endpoint: '/v3/messages', @@ -627,16 +670,17 @@ const EMBEDDED_METHODS: MethodEntry[] = [ stainlessPath: '(resource) messages > (method) create', qualified: 'client.messages.create', params: [ - "message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; };", + "message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; mention?: string; mention_range?: number[]; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; };", 'to: string[];', 'continuation_message?: { text: string; };', 'exclude_from?: string[];', + 'override_optout?: boolean;', 'Idempotency-Key?: string;', ], response: "{ chat_id: string; created_new_chat: boolean; from: string; from_selection: { reason: 'reused_active_chat' | 'new_best_number' | 'failover_flagged'; reused_existing_chat: boolean; }; handles: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }[]; is_group: boolean; message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: text_part_response | media_part_response | link_part_response | object[]; sent_at: string; delivered_at?: string; effect?: message_effect; from_handle?: chat_handle; preferred_service?: service_type; reply_to?: reply_to; service?: service_type; }; service: 'iMessage' | 'SMS' | 'RCS'; previous_chat_id?: string; }", markdown: - "## create\n\n`client.messages.create(message: { action?: object; effect?: message_effect; idempotency_key?: string; parts?: text_part | media_part | link_part | object[]; preferred_service?: service_type; reply_to?: reply_to; }, to: string[], continuation_message?: { text: string; }, exclude_from?: string[], Idempotency-Key?: string): { chat_id: string; created_new_chat: boolean; from: string; from_selection: object; handles: chat_handle[]; is_group: boolean; message: sent_message; service: service_type; previous_chat_id?: string; }`\n\n**post** `/v3/messages`\n\nSend a message to one or more recipients **without supplying a `from`\nnumber**. Linq resolves both the sending line and the target chat for you,\nthen returns exactly which line was used, which chat the message landed in,\nwhether a new chat was created, and every resulting message id.\n\nThis fuses \"create chat\" and \"send message\" behind a single\nmessage-centric resource. Provide only the recipients (`to`) and the\n`message`; the platform decides the rest.\n\n## How the from-number and chat are chosen\n\n- **Reuse** — if a chat with exactly these recipients already exists on a\n line that can still send, the message is sent into that chat on its\n existing line (`from_selection.reason = reused_active_chat`). The\n most-recently-active such chat wins; chats stranded on flagged lines\n (e.g. by an earlier failover) are skipped.\n- **New** — if no such chat exists, a new chat is created on the best\n available line (`from_selection.reason = new_best_number`).\n- **Failover** — if matching chats exist but none is on a line that can\n send, a **new** chat is created on a fresh best line and the flagged chat\n is abandoned (`from_selection.reason = failover_flagged`,\n `previous_chat_id` set). If you supply `continuation_message`, that\n text is sent as the single message INSTEAD of `message` (useful as a\n fresh-number-appropriate opener). Exactly one message is sent either way.\n\nRecipients (`to`) are an order-independent set: a single handle is a direct\nchat, multiple handles a group chat.\n\n## Excluding lines\n\n`exclude_from` keeps specific lines out of **this** send's line pick. It\nonly affects picking a line for a new chat — an existing chat is always\nreused on its own line, preferring a chat on a non-excluded line when the\nrecipients have more than one. An exclusion never abandons a live chat or\nmoves it to a new number, so if the only chat these recipients have is on\nan excluded line, that chat is still used. `from` tells you the line that\nwas actually used.\n\n## Differences from POST /v3/chats\n\n- The first message **may contain a link** (including for a newly created\n chat). Note: sending a link as the very first message on a freshly\n selected line can elevate that line's flagging risk — it is allowed, not\n recommended.\n- Voice memos are **not** supported here. To send an iMessage voice-memo\n bubble, use `POST /v3/chats/{chatId}/voicememo` with a known chat id.\n\n## Service preference, effects, decorations\n\nSet `message.preferred_service` (`iMessage` | `RCS` | `SMS`), `message.effect`,\nand per-part `text_decorations` exactly as on the other send endpoints.\n\nAlways responds `202 Accepted` — chat creation is incidental to the send.\n\n\n### Parameters\n\n- `message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; }`\n Message content container. Groups all message-related fields together,\nseparating the \"what\" (message content) from the \"where\" (routing fields like from/to).\n\nA message carries EITHER `parts` — text and attachments, which compose\ninto one bubble — or a single `action`, which invokes an experience\ninside Linq's iMessage app. Never both: an app card is the whole message\n(Apple's `MSMessage` cannot coexist with text), so copy and a card are\ntwo sends, not one.\n\n - `action?: { action: string; experience: string; params?: object; }`\n Invokes an action on an experience — a third party that renders inside\nLinq's iMessage app. Linq resolves the recipient's connection, mints any\nsession the action needs, composes the card and sends it; none of that\nis visible to you.\n\nCall `GET /v3/experiences/{experience}` for the actions you may invoke\nand the fields each accepts.\n\n - `effect?: { name?: string; type?: 'screen' | 'bubble'; }`\n iMessage effect to apply to this message (screen or bubble effect)\n - `idempotency_key?: string`\n Optional idempotency key for this message.\nUse this to prevent duplicate sends of the same message. Reusing a key\nwhose message was deleted — or was an ephemeral message that has since\nexpired — returns 404; the message is never resent.\n\n - `parts?: { type: 'text'; value: string; text_decorations?: { range: number[]; animation?: 'big' | 'small' | 'shake' | 'nod' | 'explode' | 'ripple' | 'bloom' | 'jitter'; style?: 'bold' | 'italic' | 'strikethrough' | 'underline'; }[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]`\n Array of message parts. Each part can be text, media, or link.\nParts are displayed in order. Text and media can be mixed freely,\nbut a `link` part must be the only part in the message.\n\n**Rich Link Previews:**\n- Use a `link` part to send a URL with a rich preview card\n- A `link` part must be the **only** part in the message\n- To send a URL as plain text (no preview), use a `text` part instead\n\n**Supported Media:**\n- Images: .jpg, .jpeg, .png, .gif, .heic, .heif, .tif, .tiff, .bmp\n- Videos: .mp4, .mov, .m4v, .mpeg, .mpg, .3gp\n- Audio: .m4a, .mp3, .aac, .caf, .wav, .aiff, .amr\n- Documents: .pdf, .txt, .rtf, .csv, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pages, .numbers, .key, .epub, .zip, .html, .htm\n- Contact & Calendar: .vcf, .ics\n\n**Audio:**\n- Audio files (.m4a, .mp3, .aac, .caf, .wav, .aiff, .amr) are fully supported as media parts\n- To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated\n `/v3/chats/{chatId}/voicememo` endpoint instead\n\n**Validation Rules:**\n- A `link` part must be the **only** part in the message. It cannot be combined\n with text or media parts.\n- Consecutive text parts are not allowed. Text parts must be separated by\n media parts. For example, [text, text] is invalid, but [text, media, text] is valid.\n- Maximum of **100 parts** total.\n- Media parts using a public `url` (downloaded by the server on send) are\n capped at **40**. Parts using `attachment_id` or presigned URLs\n are exempt from this sub-limit. For bulk media sends exceeding 40 files,\n pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`.\n\n - `preferred_service?: 'iMessage' | 'SMS' | 'RCS'`\n Messaging service type\n - `reply_to?: { message_id: string; part_index?: number; }`\n Reply to another message to create a threaded conversation\n\n- `to: string[]`\n Recipient handles (E.164 phone numbers or email addresses). One handle\nis a direct chat; multiple handles a group chat. Order-independent — the\nset identifies the chat.\n\n\n- `continuation_message?: { text: string; }`\n Text-only fallback that **replaces** `message` ONLY on the failover branch —\nwhen a chat with these recipients already existed but its line was flagged,\nso a new chat is created on a fresh line. On that branch this text is sent as\nthe single message instead of `message` (the recipient is on a new number, so\nyou typically want a fresh-number-appropriate opener rather than the original\ncontent). Ignored otherwise (a healthy reuse, or genuine first contact).\nCarries no parts, media, or effects — exactly one message is ever sent.\n\n - `text: string`\n The replacement message text, sent as the single message on failover.\n\n- `exclude_from?: string[]`\n Lines (E.164) not to pick for this send. Applies for this request\nonly — nothing is remembered between calls.\n\n**Exclusion only affects picking a line for a new chat.** If `to`\nalready has a chat, that chat is reused on its own line, and a chat on\na non-excluded line is preferred when there is more than one. If the\nonly chat these recipients have is on an excluded line, it is still\nreused — an exclusion never abandons a live chat or moves it to a new\nnumber. Check `from` in the response to see the line that was actually\nused.\n\nNumbers that are not your lines are ignored. Every entry must be\nE.164 — a value like `4155551234` is rejected rather than silently\nskipped. Excluding every one of your available lines returns 400 when\na line has to be picked.\n\n\n- `Idempotency-Key?: string`\n\n### Returns\n\n- `{ chat_id: string; created_new_chat: boolean; from: string; from_selection: { reason: 'reused_active_chat' | 'new_best_number' | 'failover_flagged'; reused_existing_chat: boolean; }; handles: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }[]; is_group: boolean; message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: text_part_response | media_part_response | link_part_response | object[]; sent_at: string; delivered_at?: string; effect?: message_effect; from_handle?: chat_handle; preferred_service?: service_type; reply_to?: reply_to; service?: service_type; }; service: 'iMessage' | 'SMS' | 'RCS'; previous_chat_id?: string; }`\n Result of an auto-from send. Self-describing: which line was used, which\nchat the message landed in, whether a new chat was created, and the\nresulting message id(s).\n\n\n - `chat_id: string`\n - `created_new_chat: boolean`\n - `from: string`\n - `from_selection: { reason: 'reused_active_chat' | 'new_best_number' | 'failover_flagged'; reused_existing_chat: boolean; }`\n - `handles: { id: string; handle: string; joined_at: string; service: 'iMessage' | 'SMS' | 'RCS'; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }[]`\n - `is_group: boolean`\n - `message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: { reactions: reaction[]; type: 'text'; value: string; text_decorations?: text_decoration[]; } | { id: string; filename: string; mime_type: string; reactions: reaction[]; size_bytes: number; type: 'media'; url: string; } | { reactions: reaction[]; type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; reactions: object[]; type: 'imessage_app'; url: string; fallback_text?: string; }[]; sent_at: string; delivered_at?: string; effect?: { name?: string; type?: 'screen' | 'bubble'; }; from_handle?: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; service?: 'iMessage' | 'SMS' | 'RCS'; }`\n - `service: 'iMessage' | 'SMS' | 'RCS'`\n - `previous_chat_id?: string`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst message = await client.messages.create({\n message: {},\n to: ['+14155559876'],\n});\n\nconsole.log(message);\n```", + "## create\n\n`client.messages.create(message: { action?: object; effect?: message_effect; idempotency_key?: string; parts?: text_part | media_part | link_part | object[]; preferred_service?: service_type; reply_to?: reply_to; }, to: string[], continuation_message?: { text: string; }, exclude_from?: string[], override_optout?: boolean, Idempotency-Key?: string): { chat_id: string; created_new_chat: boolean; from: string; from_selection: object; handles: chat_handle[]; is_group: boolean; message: sent_message; service: service_type; previous_chat_id?: string; }`\n\n**post** `/v3/messages`\n\nSend a message to one or more recipients **without supplying a `from`\nnumber**. Linq resolves both the sending line and the target chat for you,\nthen returns exactly which line was used, which chat the message landed in,\nwhether a new chat was created, and every resulting message id.\n\nThis fuses \"create chat\" and \"send message\" behind a single\nmessage-centric resource. Provide only the recipients (`to`) and the\n`message`; the platform decides the rest.\n\n## How the from-number and chat are chosen\n\n- **Reuse** — if a chat with exactly these recipients already exists on a\n line that can still send, the message is sent into that chat on its\n existing line (`from_selection.reason = reused_active_chat`). The\n most-recently-active such chat wins; chats stranded on flagged lines\n (e.g. by an earlier failover) are skipped.\n- **New** — if no such chat exists, a new chat is created on the best\n available line (`from_selection.reason = new_best_number`).\n- **Failover** — if matching chats exist but none is on a line that can\n send, a **new** chat is created on a fresh best line and the flagged chat\n is abandoned (`from_selection.reason = failover_flagged`,\n `previous_chat_id` set). If you supply `continuation_message`, that\n text is sent as the single message INSTEAD of `message` (useful as a\n fresh-number-appropriate opener). Exactly one message is sent either way.\n\nRecipients (`to`) are an order-independent set: a single handle is a direct\nchat, multiple handles a group chat.\n\n## Excluding lines\n\n`exclude_from` keeps specific lines out of **this** send's line pick. It\nonly affects picking a line for a new chat — an existing chat is always\nreused on its own line, preferring a chat on a non-excluded line when the\nrecipients have more than one. An exclusion never abandons a live chat or\nmoves it to a new number, so if the only chat these recipients have is on\nan excluded line, that chat is still used. `from` tells you the line that\nwas actually used.\n\n## Differences from POST /v3/chats\n\n- The first message **may contain a link** (including for a newly created\n chat). Note: sending a link as the very first message on a freshly\n selected line can elevate that line's flagging risk — it is allowed, not\n recommended.\n- Voice memos are **not** supported here. To send an iMessage voice-memo\n bubble, use `POST /v3/chats/{chatId}/voicememo` with a known chat id.\n\n## Service preference, effects, decorations\n\nSet `message.preferred_service` (`iMessage` | `RCS` | `SMS`), `message.effect`,\nand per-part `text_decorations` exactly as on the other send endpoints.\n\nAlways responds `202 Accepted` — chat creation is incidental to the send.\n\n\n### Parameters\n\n- `message: { action?: { action: string; experience: string; params?: object; }; effect?: { name?: string; type?: 'screen' | 'bubble'; }; idempotency_key?: string; parts?: { type: 'text'; value: string; mention?: string; mention_range?: number[]; text_decorations?: text_decoration[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; }`\n Message content container. Groups all message-related fields together,\nseparating the \"what\" (message content) from the \"where\" (routing fields like from/to).\n\nA message carries EITHER `parts` — text and attachments, which compose\ninto one bubble — or a single `action`, which invokes an experience\ninside Linq's iMessage app. Never both: an app card is the whole message\n(Apple's `MSMessage` cannot coexist with text), so copy and a card are\ntwo sends, not one.\n\n - `action?: { action: string; experience: string; params?: object; }`\n Invokes an action on an experience — a third party that renders inside\nLinq's iMessage app. Linq resolves the recipient's connection, mints any\nsession the action needs, composes the card and sends it; none of that\nis visible to you.\n\nCall `GET /v3/experiences/{experience}` for the actions you may invoke\nand the fields each accepts.\n\n - `effect?: { name?: string; type?: 'screen' | 'bubble'; }`\n iMessage effect to apply to this message (screen or bubble effect)\n - `idempotency_key?: string`\n Optional idempotency key for this message.\nUse this to prevent duplicate sends of the same message. Reusing a key\nwhose message was deleted — or was an ephemeral message that has since\nexpired — returns 404; the message is never resent.\n\n - `parts?: { type: 'text'; value: string; mention?: string; mention_range?: number[]; text_decorations?: { range: number[]; animation?: 'big' | 'small' | 'shake' | 'nod' | 'explode' | 'ripple' | 'bloom' | 'jitter'; style?: 'bold' | 'italic' | 'strikethrough' | 'underline'; }[]; } | { type: 'media'; attachment_id?: string; url?: string; } | { type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; type: 'imessage_app'; fallback_text?: string; interactive?: boolean; url?: string; }[]`\n Array of message parts. Each part can be text, media, or link.\nParts are displayed in order. Text and media can be mixed freely,\nbut a `link` part must be the only part in the message.\n\n**Rich Link Previews:**\n- Use a `link` part to send a URL with a rich preview card\n- A `link` part must be the **only** part in the message\n- To send a URL as plain text (no preview), use a `text` part instead\n\n**Supported Media:**\n- Images: .jpg, .jpeg, .png, .gif, .heic, .heif, .tif, .tiff, .bmp\n- Videos: .mp4, .mov, .m4v, .mpeg, .mpg, .3gp\n- Audio: .m4a, .mp3, .aac, .caf, .wav, .aiff, .amr\n- Documents: .pdf, .txt, .rtf, .csv, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .pages, .numbers, .key, .epub, .zip, .html, .htm\n- Contact & Calendar: .vcf, .ics\n\n**Audio:**\n- Audio files (.m4a, .mp3, .aac, .caf, .wav, .aiff, .amr) are fully supported as media parts\n- To send audio as an **iMessage voice memo bubble** (inline playback UI), use the dedicated\n `/v3/chats/{chatId}/voicememo` endpoint instead\n\n**Validation Rules:**\n- A `link` part must be the **only** part in the message. It cannot be combined\n with text or media parts.\n- Consecutive text parts are not allowed. Text parts must be separated by\n media parts. For example, [text, text] is invalid, but [text, media, text] is valid.\n- Maximum of **100 parts** total.\n- Media parts using a public `url` (downloaded by the server on send) are\n capped at **40**. Parts using `attachment_id` or presigned URLs\n are exempt from this sub-limit. For bulk media sends exceeding 40 files,\n pre-upload via `POST /v3/attachments` and reference by `attachment_id` or `download_url`.\n\n - `preferred_service?: 'iMessage' | 'SMS' | 'RCS'`\n Messaging service type\n - `reply_to?: { message_id: string; part_index?: number; }`\n Reply to another message to create a threaded conversation\n\n- `to: string[]`\n Recipient handles (E.164 phone numbers or email addresses). One handle\nis a direct chat; multiple handles a group chat. Order-independent — the\nset identifies the chat.\n\n\n- `continuation_message?: { text: string; }`\n Text-only fallback that **replaces** `message` ONLY on the failover branch —\nwhen a chat with these recipients already existed but its line was flagged,\nso a new chat is created on a fresh line. On that branch this text is sent as\nthe single message instead of `message` (the recipient is on a new number, so\nyou typically want a fresh-number-appropriate opener rather than the original\ncontent). Ignored otherwise (a healthy reuse, or genuine first contact).\nCarries no parts, media, or effects — exactly one message is ever sent.\n\n - `text: string`\n The replacement message text, sent as the single message on failover.\n\n- `exclude_from?: string[]`\n Lines (E.164) not to pick for this send. Applies for this request\nonly — nothing is remembered between calls.\n\n**Exclusion only affects picking a line for a new chat.** If `to`\nalready has a chat, that chat is reused on its own line, and a chat on\na non-excluded line is preferred when there is more than one. If the\nonly chat these recipients have is on an excluded line, it is still\nreused — an exclusion never abandons a live chat or moves it to a new\nnumber. Check `from` in the response to see the line that was actually\nused.\n\nNumbers that are not your lines are ignored. Every entry must be\nE.164 — a value like `4155551234` is rejected rather than silently\nskipped. Excluding every one of your available lines returns 400 when\na line has to be picked.\n\n\n- `override_optout?: boolean`\n Send even though the recipient asked you to stop (`403`, error code\n`2024`). Applies to this request only: the opt-out stays in place, so\nthe next send without this flag is rejected again. Every override is\nrecorded against your API key.\n\n\n- `Idempotency-Key?: string`\n\n### Returns\n\n- `{ chat_id: string; created_new_chat: boolean; from: string; from_selection: { reason: 'reused_active_chat' | 'new_best_number' | 'failover_flagged'; reused_existing_chat: boolean; }; handles: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }[]; is_group: boolean; message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: text_part_response | media_part_response | link_part_response | object[]; sent_at: string; delivered_at?: string; effect?: message_effect; from_handle?: chat_handle; preferred_service?: service_type; reply_to?: reply_to; service?: service_type; }; service: 'iMessage' | 'SMS' | 'RCS'; previous_chat_id?: string; }`\n Result of an auto-from send. Self-describing: which line was used, which\nchat the message landed in, whether a new chat was created, and the\nresulting message id(s).\n\n\n - `chat_id: string`\n - `created_new_chat: boolean`\n - `from: string`\n - `from_selection: { reason: 'reused_active_chat' | 'new_best_number' | 'failover_flagged'; reused_existing_chat: boolean; }`\n - `handles: { id: string; handle: string; joined_at: string; service: 'iMessage' | 'SMS' | 'RCS'; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }[]`\n - `is_group: boolean`\n - `message: { id: string; created_at: string; delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; is_read: boolean; parts: { reactions: reaction[]; type: 'text'; value: string; text_decorations?: text_decoration[]; } | { id: string; filename: string; mime_type: string; reactions: reaction[]; size_bytes: number; type: 'media'; url: string; } | { reactions: reaction[]; type: 'link'; value: string; } | { app: { bundle_id: string; name: string; team_id: string; app_store_id?: number; }; layout: { caption?: string; image_subtitle?: string; image_title?: string; image_url?: string; subcaption?: string; trailing_caption?: string; trailing_subcaption?: string; }; reactions: object[]; type: 'imessage_app'; url: string; fallback_text?: string; }[]; sent_at: string; delivered_at?: string; effect?: { name?: string; type?: 'screen' | 'bubble'; }; from_handle?: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }; preferred_service?: 'iMessage' | 'SMS' | 'RCS'; reply_to?: { message_id: string; part_index?: number; }; service?: 'iMessage' | 'SMS' | 'RCS'; }`\n - `service: 'iMessage' | 'SMS' | 'RCS'`\n - `previous_chat_id?: string`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst message = await client.messages.create({\n message: {},\n to: ['+14155559876'],\n});\n\nconsole.log(message);\n```", perLanguage: { go: { method: 'client.Messages.New', @@ -655,7 +699,7 @@ const EMBEDDED_METHODS: MethodEntry[] = [ }, http: { example: - 'curl https://api.linqapp.com/api/partner/v3/messages \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "message": {\n "parts": [\n {\n "type": "text",\n "value": "Hi! Thanks for reaching out — how can we help?"\n }\n ]\n },\n "to": [\n "+14155559876"\n ],\n "exclude_from": [\n "+12052535597"\n ]\n }\'', + 'curl https://api.linqapp.com/api/partner/v3/messages \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "message": {\n "parts": [\n {\n "type": "text",\n "value": "Hi! Thanks for reaching out — how can we help?"\n }\n ]\n },\n "to": [\n "+14155559876"\n ],\n "exclude_from": [\n "+12052535597"\n ],\n "override_optout": false\n }\'', }, }, }, @@ -884,6 +928,114 @@ const EMBEDDED_METHODS: MethodEntry[] = [ }, }, }, + { + name: 'retrieve', + endpoint: '/v3/messages/{messageId}/poll', + httpMethod: 'get', + summary: "Get a poll's current tally", + description: + "Return a poll's current results — its options, each option's voters, and the distinct\ntotal number of voters — by the poll-definition message's ID.\n", + stainlessPath: '(resource) messages.poll > (method) retrieve', + qualified: 'client.messages.poll.retrieve', + params: ['messageId: string;'], + response: + '{ chat_id: string; created_at: string; message_id: string; poll: { options: object[]; total_voters: number; }; reactions: { handle: chat_handle; is_me: boolean; type: reaction_type; custom_emoji?: string; sticker?: object; }[]; updated_at: string; }', + markdown: + "## retrieve\n\n`client.messages.poll.retrieve(messageId: string): { chat_id: string; created_at: string; message_id: string; poll: poll; reactions: reaction[]; updated_at: string; }`\n\n**get** `/v3/messages/{messageId}/poll`\n\nReturn a poll's current results — its options, each option's voters, and the distinct\ntotal number of voters — by the poll-definition message's ID.\n\n\n### Parameters\n\n- `messageId: string`\n\n### Returns\n\n- `{ chat_id: string; created_at: string; message_id: string; poll: { options: object[]; total_voters: number; }; reactions: { handle: chat_handle; is_me: boolean; type: reaction_type; custom_emoji?: string; sticker?: object; }[]; updated_at: string; }`\n Message-level envelope returned by every poll endpoint.\n\n - `chat_id: string`\n - `created_at: string`\n - `message_id: string`\n - `poll: { options: { can_be_edited: boolean; creator_handle: object; option_id: string; text: string; voters: { handle: string; voted_at: string; }[]; }[]; total_voters: number; }`\n - `reactions: { handle: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }; is_me: boolean; type: 'love' | 'like' | 'dislike' | 'laugh' | 'emphasize' | 'question' | 'custom' | 'sticker'; custom_emoji?: string; sticker?: { file_name?: string; height?: number; mime_type?: string; url?: string; width?: number; }; }[]`\n - `updated_at: string`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst pollEnvelope = await client.messages.poll.retrieve('69a37c7d-af4f-4b5e-af42-e28e98ce873a');\n\nconsole.log(pollEnvelope);\n```", + perLanguage: { + go: { + method: 'client.Messages.Poll.Get', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/linq-team/linq-go"\n\t"github.com/linq-team/linq-go/option"\n)\n\nfunc main() {\n\tclient := linqgo.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpollEnvelope, err := client.Messages.Poll.Get(context.TODO(), "69a37c7d-af4f-4b5e-af42-e28e98ce873a")\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", pollEnvelope.ChatID)\n}\n', + }, + python: { + method: 'messages.poll.retrieve', + example: + 'import os\nfrom linq import LinqAPIV3\n\nclient = LinqAPIV3(\n api_key=os.environ.get("LINQ_API_V3_API_KEY"), # This is the default and can be omitted\n)\npoll_envelope = client.messages.poll.retrieve(\n "69a37c7d-af4f-4b5e-af42-e28e98ce873a",\n)\nprint(poll_envelope.chat_id)', + }, + typescript: { + method: 'client.messages.poll.retrieve', + example: + "import LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3({\n apiKey: process.env['LINQ_API_V3_API_KEY'], // This is the default and can be omitted\n});\n\nconst pollEnvelope = await client.messages.poll.retrieve('69a37c7d-af4f-4b5e-af42-e28e98ce873a');\n\nconsole.log(pollEnvelope.chat_id);", + }, + http: { + example: + 'curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID/poll \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY"', + }, + }, + }, + { + name: 'add_options', + endpoint: '/v3/messages/{messageId}/poll/options', + httpMethod: 'post', + summary: 'Add options to a poll', + description: + 'Add one or more options to an existing poll. Options are **add-only and immutable** — you\ncan append options but never edit or remove them (Apple constraint). Returns the full poll.\n', + stainlessPath: '(resource) messages.poll > (method) add_options', + qualified: 'client.messages.poll.addOptions', + params: ['messageId: string;', 'options: { text: string; }[];'], + response: + '{ chat_id: string; created_at: string; message_id: string; poll: { options: object[]; total_voters: number; }; reactions: { handle: chat_handle; is_me: boolean; type: reaction_type; custom_emoji?: string; sticker?: object; }[]; updated_at: string; }', + markdown: + "## add_options\n\n`client.messages.poll.addOptions(messageId: string, options: { text: string; }[]): { chat_id: string; created_at: string; message_id: string; poll: poll; reactions: reaction[]; updated_at: string; }`\n\n**post** `/v3/messages/{messageId}/poll/options`\n\nAdd one or more options to an existing poll. Options are **add-only and immutable** — you\ncan append options but never edit or remove them (Apple constraint). Returns the full poll.\n\n\n### Parameters\n\n- `messageId: string`\n\n- `options: { text: string; }[]`\n\n### Returns\n\n- `{ chat_id: string; created_at: string; message_id: string; poll: { options: object[]; total_voters: number; }; reactions: { handle: chat_handle; is_me: boolean; type: reaction_type; custom_emoji?: string; sticker?: object; }[]; updated_at: string; }`\n Message-level envelope returned by every poll endpoint.\n\n - `chat_id: string`\n - `created_at: string`\n - `message_id: string`\n - `poll: { options: { can_be_edited: boolean; creator_handle: object; option_id: string; text: string; voters: { handle: string; voted_at: string; }[]; }[]; total_voters: number; }`\n - `reactions: { handle: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }; is_me: boolean; type: 'love' | 'like' | 'dislike' | 'laugh' | 'emphasize' | 'question' | 'custom' | 'sticker'; custom_emoji?: string; sticker?: { file_name?: string; height?: number; mime_type?: string; url?: string; width?: number; }; }[]`\n - `updated_at: string`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst pollEnvelope = await client.messages.poll.addOptions('69a37c7d-af4f-4b5e-af42-e28e98ce873a', { options: [{ text: 'Pizza' }] });\n\nconsole.log(pollEnvelope);\n```", + perLanguage: { + go: { + method: 'client.Messages.Poll.AddOptions', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/linq-team/linq-go"\n\t"github.com/linq-team/linq-go/option"\n)\n\nfunc main() {\n\tclient := linqgo.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpollEnvelope, err := client.Messages.Poll.AddOptions(\n\t\tcontext.TODO(),\n\t\t"69a37c7d-af4f-4b5e-af42-e28e98ce873a",\n\t\tlinqgo.MessagePollAddOptionsParams{\n\t\t\tOptions: []linqgo.MessagePollAddOptionsParamsOption{{\n\t\t\t\tText: "Pizza",\n\t\t\t}},\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", pollEnvelope.ChatID)\n}\n', + }, + python: { + method: 'messages.poll.add_options', + example: + 'import os\nfrom linq import LinqAPIV3\n\nclient = LinqAPIV3(\n api_key=os.environ.get("LINQ_API_V3_API_KEY"), # This is the default and can be omitted\n)\npoll_envelope = client.messages.poll.add_options(\n message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a",\n options=[{\n "text": "Pizza"\n }],\n)\nprint(poll_envelope.chat_id)', + }, + typescript: { + method: 'client.messages.poll.addOptions', + example: + "import LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3({\n apiKey: process.env['LINQ_API_V3_API_KEY'], // This is the default and can be omitted\n});\n\nconst pollEnvelope = await client.messages.poll.addOptions('69a37c7d-af4f-4b5e-af42-e28e98ce873a', {\n options: [{ text: 'Pizza' }],\n});\n\nconsole.log(pollEnvelope.chat_id);", + }, + http: { + example: + 'curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID/poll/options \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "options": [\n {\n "text": "Pizza"\n }\n ]\n }\'', + }, + }, + }, + { + name: 'vote', + endpoint: '/v3/messages/{messageId}/poll/votes', + httpMethod: 'post', + summary: 'Toggle a vote on a poll option', + description: + "Add or remove your line's vote on **one** poll option (per-option toggle — iMessage polls\nare toggled one option at a time). Returns the poll reflecting the toggle.\n", + stainlessPath: '(resource) messages.poll > (method) vote', + qualified: 'client.messages.poll.vote', + params: ['messageId: string;', "operation: 'add' | 'remove';", 'option_id: string;'], + response: + '{ chat_id: string; created_at: string; message_id: string; poll: { options: object[]; total_voters: number; }; reactions: { handle: chat_handle; is_me: boolean; type: reaction_type; custom_emoji?: string; sticker?: object; }[]; updated_at: string; }', + markdown: + "## vote\n\n`client.messages.poll.vote(messageId: string, operation: 'add' | 'remove', option_id: string): { chat_id: string; created_at: string; message_id: string; poll: poll; reactions: reaction[]; updated_at: string; }`\n\n**post** `/v3/messages/{messageId}/poll/votes`\n\nAdd or remove your line's vote on **one** poll option (per-option toggle — iMessage polls\nare toggled one option at a time). Returns the poll reflecting the toggle.\n\n\n### Parameters\n\n- `messageId: string`\n\n- `operation: 'add' | 'remove'`\n Add or remove your line's vote on the option.\n\n- `option_id: string`\n The option to toggle a vote on.\n\n### Returns\n\n- `{ chat_id: string; created_at: string; message_id: string; poll: { options: object[]; total_voters: number; }; reactions: { handle: chat_handle; is_me: boolean; type: reaction_type; custom_emoji?: string; sticker?: object; }[]; updated_at: string; }`\n Message-level envelope returned by every poll endpoint.\n\n - `chat_id: string`\n - `created_at: string`\n - `message_id: string`\n - `poll: { options: { can_be_edited: boolean; creator_handle: object; option_id: string; text: string; voters: { handle: string; voted_at: string; }[]; }[]; total_voters: number; }`\n - `reactions: { handle: { id: string; handle: string; joined_at: string; service: service_type; is_me?: boolean; left_at?: string; status?: 'active' | 'left' | 'removed'; }; is_me: boolean; type: 'love' | 'like' | 'dislike' | 'laugh' | 'emphasize' | 'question' | 'custom' | 'sticker'; custom_emoji?: string; sticker?: { file_name?: string; height?: number; mime_type?: string; url?: string; width?: number; }; }[]`\n - `updated_at: string`\n\n### Example\n\n```typescript\nimport LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3();\n\nconst pollEnvelope = await client.messages.poll.vote('69a37c7d-af4f-4b5e-af42-e28e98ce873a', { operation: 'add', option_id: '97ce8c17-7ef6-4bbc-a89a-6b93d189712f' });\n\nconsole.log(pollEnvelope);\n```", + perLanguage: { + go: { + method: 'client.Messages.Poll.Vote', + example: + 'package main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/linq-team/linq-go"\n\t"github.com/linq-team/linq-go/option"\n)\n\nfunc main() {\n\tclient := linqgo.NewClient(\n\t\toption.WithAPIKey("My API Key"),\n\t)\n\tpollEnvelope, err := client.Messages.Poll.Vote(\n\t\tcontext.TODO(),\n\t\t"69a37c7d-af4f-4b5e-af42-e28e98ce873a",\n\t\tlinqgo.MessagePollVoteParams{\n\t\t\tOperation: linqgo.MessagePollVoteParamsOperationAdd,\n\t\t\tOptionID: "97ce8c17-7ef6-4bbc-a89a-6b93d189712f",\n\t\t},\n\t)\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", pollEnvelope.ChatID)\n}\n', + }, + python: { + method: 'messages.poll.vote', + example: + 'import os\nfrom linq import LinqAPIV3\n\nclient = LinqAPIV3(\n api_key=os.environ.get("LINQ_API_V3_API_KEY"), # This is the default and can be omitted\n)\npoll_envelope = client.messages.poll.vote(\n message_id="69a37c7d-af4f-4b5e-af42-e28e98ce873a",\n operation="add",\n option_id="97ce8c17-7ef6-4bbc-a89a-6b93d189712f",\n)\nprint(poll_envelope.chat_id)', + }, + typescript: { + method: 'client.messages.poll.vote', + example: + "import LinqAPIV3 from '@linqapp/sdk';\n\nconst client = new LinqAPIV3({\n apiKey: process.env['LINQ_API_V3_API_KEY'], // This is the default and can be omitted\n});\n\nconst pollEnvelope = await client.messages.poll.vote('69a37c7d-af4f-4b5e-af42-e28e98ce873a', {\n operation: 'add',\n option_id: '97ce8c17-7ef6-4bbc-a89a-6b93d189712f',\n});\n\nconsole.log(pollEnvelope.chat_id);", + }, + http: { + example: + 'curl https://api.linqapp.com/api/partner/v3/messages/$MESSAGE_ID/poll/votes \\\n -H \'Content-Type: application/json\' \\\n -H "Authorization: Bearer $LINQ_API_V3_API_KEY" \\\n -d \'{\n "operation": "add",\n "option_id": "97ce8c17-7ef6-4bbc-a89a-6b93d189712f"\n }\'', + }, + }, + }, { name: 'create', endpoint: '/v3/attachments', @@ -2145,7 +2297,7 @@ const EMBEDDED_READMES: { language: string; content: string }[] = [ { language: 'go', content: - '# Linq API V3 Go API Library\n\nGo Reference\n\nThe Linq API V3 Go library provides convenient access to the [Linq API V3 REST API](https://docs.linqapp.com)\nfrom applications written in Go.\n\nIt is generated with [Stainless](https://www.stainless.com/).\n\n## MCP Server\n\nUse the Linq API V3 MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.\n\n[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40linqapp%2Fsdk-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaW5xYXBwL3Nkay1tY3AiXSwiZW52Ijp7IkxJTlFfQVBJX1YzX0FQSV9LRVkiOiJNeSBBUEkgS2V5IiwiTElOUV9XRUJIT09LX1NFQ1JFVCI6Ik15IFdlYmhvb2sgU2VjcmV0In19)\n[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40linqapp%2Fsdk-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40linqapp%2Fsdk-mcp%22%5D%2C%22env%22%3A%7B%22LINQ_API_V3_API_KEY%22%3A%22My%20API%20Key%22%2C%22LINQ_WEBHOOK_SECRET%22%3A%22My%20Webhook%20Secret%22%7D%7D)\n\n> Note: You may need to set environment variables in your MCP client.\n\n## Installation\n\n\n\n```go\nimport (\n\t"github.com/linq-team/linq-go" // imported as SDK_PackageName\n)\n```\n\n\n\nOr to pin the version:\n\n\n\n```sh\ngo get -u \'github.com/linq-team/linq-go@v0.28.0\'\n```\n\n\n\n## Requirements\n\nThis library requires Go 1.22+.\n\n## Usage\n\nThe full API of this library can be found in [api.md](api.md).\n\n```go\npackage main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/linq-team/linq-go"\n\t"github.com/linq-team/linq-go/option"\n)\n\nfunc main() {\n\tclient := linqgo.NewClient(\n\t\toption.WithAPIKey("My API Key"), // defaults to os.LookupEnv("LINQ_API_V3_API_KEY")\n\t)\n\tchat, err := client.Chats.New(context.TODO(), linqgo.ChatNewParams{\n\t\tFrom: "+12052535597",\n\t\tMessage: linqgo.MessageContentParam{},\n\t\tTo: []string{"+12052532136"},\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", chat.Chat)\n}\n\n```\n\n### Request fields\n\nAll request parameters are wrapped in a generic `Field` type,\nwhich we use to distinguish zero values from null or omitted fields.\n\nThis prevents accidentally sending a zero value if you forget a required parameter,\nand enables explicitly sending `null`, `false`, `\'\'`, or `0` on optional parameters.\nAny field not specified is not sent.\n\nTo construct fields with values, use the helpers `String()`, `Int()`, `Float()`, or most commonly, the generic `F[T]()`.\nTo send a null, use `Null[T]()`, and to send a nonconforming value, use `Raw[T](any)`. For example:\n\n```go\nparams := FooParams{\n\tName: SDK_PackageName.F("hello"),\n\n\t// Explicitly send `"description": null`\n\tDescription: SDK_PackageName.Null[string](),\n\n\tPoint: SDK_PackageName.F(SDK_PackageName.Point{\n\t\tX: SDK_PackageName.Int(0),\n\t\tY: SDK_PackageName.Int(1),\n\n\t\t// In cases where the API specifies a given type,\n\t\t// but you want to send something else, use `Raw`:\n\t\tZ: SDK_PackageName.Raw[int64](0.01), // sends a float\n\t}),\n}\n```\n\n### Response objects\n\nAll fields in response structs are value types (not pointers or wrappers).\n\nIf a given field is `null`, not present, or invalid, the corresponding field\nwill simply be its zero value.\n\nAll response structs also include a special `JSON` field, containing more detailed\ninformation about each property, which you can use like so:\n\n```go\nif res.Name == "" {\n\t// true if `"name"` is either not present or explicitly null\n\tres.JSON.Name.IsNull()\n\n\t// true if the `"name"` key was not present in the response JSON at all\n\tres.JSON.Name.IsMissing()\n\n\t// When the API returns data that cannot be coerced to the expected type:\n\tif res.JSON.Name.IsInvalid() {\n\t\traw := res.JSON.Name.Raw()\n\n\t\tlegacyName := struct{\n\t\t\tFirst string `json:"first"`\n\t\t\tLast string `json:"last"`\n\t\t}{}\n\t\tjson.Unmarshal([]byte(raw), &legacyName)\n\t\tname = legacyName.First + " " + legacyName.Last\n\t}\n}\n```\n\nThese `.JSON` structs also include an `Extras` map containing\nany properties in the json response that were not specified\nin the struct. This can be useful for API features not yet\npresent in the SDK.\n\n```go\nbody := res.JSON.ExtraFields["my_unexpected_field"].Raw()\n```\n\n### RequestOptions\n\nThis library uses the functional options pattern. Functions defined in the\n`SDK_PackageOptionName` package return a `RequestOption`, which is a closure that mutates a\n`RequestConfig`. These options can be supplied to the client or at individual\nrequests. For example:\n\n```go\nclient := SDK_PackageName.SDK_ClientInitializerName(\n\t// Adds a header to every request made by the client\n\tSDK_PackageOptionName.WithHeader("X-Some-Header", "custom_header_info"),\n)\n\nclient.Chats.New(context.TODO(), ...,\n\t// Override the header\n\tSDK_PackageOptionName.WithHeader("X-Some-Header", "some_other_custom_header_info"),\n\t// Add an undocumented field to the request body, using sjson syntax\n\tSDK_PackageOptionName.WithJSONSet("some.json.path", map[string]string{"my": "object"}),\n)\n```\n\nSee the [full list of request options](https://pkg.go.dev/github.com/linq-team/linq-go/SDK_PackageOptionName).\n\n### Pagination\n\nThis library provides some conveniences for working with paginated list endpoints.\n\nYou can use `.ListAutoPaging()` methods to iterate through items across all pages:\n\n```go\niter := client.Chats.ListChatsAutoPaging(context.TODO(), linqgo.ChatListChatsParams{})\n// Automatically fetches more pages as needed.\nfor iter.Next() {\n\tchat := iter.Current()\n\tfmt.Printf("%+v\\n", chat)\n}\nif err := iter.Err(); err != nil {\n\tpanic(err.Error())\n}\n```\n\nOr you can use simple `.List()` methods to fetch a single page and receive a standard response object\nwith additional helper methods like `.GetNextPage()`, e.g.:\n\n```go\npage, err := client.Chats.ListChats(context.TODO(), linqgo.ChatListChatsParams{})\nfor page != nil {\n\tfor _, chat := range page.Chats {\n\t\tfmt.Printf("%+v\\n", chat)\n\t}\n\tpage, err = page.GetNextPage()\n}\nif err != nil {\n\tpanic(err.Error())\n}\n```\n\n### Errors\n\nWhen the API returns a non-success status code, we return an error with type\n`*SDK_PackageName.Error`. This contains the `StatusCode`, `*http.Request`, and\n`*http.Response` values of the request, as well as the JSON of the error body\n(much like other response objects in the SDK).\n\nTo handle errors, we recommend that you use the `errors.As` pattern:\n\n```go\n_, err := client.Chats.New(context.TODO(), linqgo.ChatNewParams{\n\tFrom: "+12052535597",\n\tMessage: linqgo.MessageContentParam{},\n\tTo: []string{"+12052532136"},\n})\nif err != nil {\n\tvar apierr *linqgo.Error\n\tif errors.As(err, &apierr) {\n\t\tprintln(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request\n\t\tprintln(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response\n\t}\n\tpanic(err.Error()) // GET "/v3/chats": 400 Bad Request { ... }\n}\n```\n\nWhen other errors occur, they are returned unwrapped; for example,\nif HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`.\n\n### Timeouts\n\nRequests do not time out by default; use context to configure a timeout for a request lifecycle.\n\nNote that if a request is [retried](#retries), the context timeout does not start over.\nTo set a per-retry timeout, use `SDK_PackageOptionName.WithRequestTimeout()`.\n\n```go\n// This sets the timeout for the request, including all the retries.\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)\ndefer cancel()\nclient.Chats.New(\n\tctx,\n\tlinqgo.ChatNewParams{\n\t\tFrom: "+12052535597",\n\t\tMessage: linqgo.MessageContentParam{},\n\t\tTo: []string{"+12052532136"},\n\t},\n\t// This sets the per-retry timeout\n\toption.WithRequestTimeout(20*time.Second),\n)\n```\n\n### File uploads\n\nRequest parameters that correspond to file uploads in multipart requests are typed as\n`param.Field[io.Reader]`. The contents of the `io.Reader` will by default be sent as a multipart form\npart with the file name of "anonymous_file" and content-type of "application/octet-stream".\n\nThe file name and content-type can be customized by implementing `Name() string` or `ContentType()\nstring` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a\nfile returned by `os.Open` will be sent with the file name on disk.\n\nWe also provide a helper `SDK_PackageName.FileParam(reader io.Reader, filename string, contentType string)`\nwhich can be used to wrap any `io.Reader` with the appropriate file name and content type.\n\n\n\n### Retries\n\nCertain errors will be automatically retried 2 times by default, with a short exponential backoff.\nWe retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit,\nand >=500 Internal errors.\n\nYou can use the `WithMaxRetries` option to configure or disable this:\n\n```go\n// Configure the default for all requests:\nclient := linqgo.NewClient(\n\toption.WithMaxRetries(0), // default is 2\n)\n\n// Override per-request:\nclient.Chats.New(\n\tcontext.TODO(),\n\tlinqgo.ChatNewParams{\n\t\tFrom: "+12052535597",\n\t\tMessage: linqgo.MessageContentParam{},\n\t\tTo: []string{"+12052532136"},\n\t},\n\toption.WithMaxRetries(5),\n)\n```\n\n\n### Accessing raw response data (e.g. response headers)\n\nYou can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when\nyou need to examine response headers, status codes, or other details.\n\n```go\n// Create a variable to store the HTTP response\nvar response *http.Response\nchat, err := client.Chats.New(\n\tcontext.TODO(),\n\tlinqgo.ChatNewParams{\n\t\tFrom: "+12052535597",\n\t\tMessage: linqgo.MessageContentParam{},\n\t\tTo: []string{"+12052532136"},\n\t},\n\toption.WithResponseInto(&response),\n)\nif err != nil {\n\t// handle error\n}\nfmt.Printf("%+v\\n", chat)\n\nfmt.Printf("Status Code: %d\\n", response.StatusCode)\nfmt.Printf("Headers: %+#v\\n", response.Header)\n```\n\n### Making custom/undocumented requests\n\nThis library is typed for convenient access to the documented API. If you need to access undocumented\nendpoints, params, or response properties, the library can still be used.\n\n#### Undocumented endpoints\n\nTo make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs.\n`RequestOptions` on the client, such as retries, will be respected when making these requests.\n\n```go\nvar (\n // params can be an io.Reader, a []byte, an encoding/json serializable object,\n // or a "…Params" struct defined in this library.\n params map[string]interface{}\n\n // result can be an []byte, *http.Response, a encoding/json deserializable object,\n // or a model defined in this library.\n result *http.Response\n)\nerr := client.Post(context.Background(), "/unspecified", params, &result)\nif err != nil {\n …\n}\n```\n\n#### Undocumented request params\n\nTo make requests using undocumented parameters, you may use either the `SDK_PackageOptionName.WithQuerySet()`\nor the `SDK_PackageOptionName.WithJSONSet()` methods.\n\n```go\nparams := FooNewParams{\n ID: SDK_PackageName.F("id_xxxx"),\n Data: SDK_PackageName.F(FooNewParamsData{\n FirstName: SDK_PackageName.F("John"),\n }),\n}\nclient.Foo.New(context.Background(), params, SDK_PackageOptionName.WithJSONSet("data.last_name", "Doe"))\n```\n\n#### Undocumented response properties\n\nTo access undocumented response properties, you may either access the raw JSON of the response as a string\nwith `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with\n`result.JSON.Foo.Raw()`.\n\nAny fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`.\n\n### Middleware\n\nWe provide `SDK_PackageOptionName.WithMiddleware` which applies the given\nmiddleware to requests.\n\n```go\nfunc Logger(req *http.Request, next SDK_PackageOptionName.MiddlewareNext) (res *http.Response, err error) {\n\t// Before the request\n\tstart := time.Now()\n\tLogReq(req)\n\n\t// Forward the request to the next handler\n\tres, err = next(req)\n\n\t// Handle stuff after the request\n\tend := time.Now()\n\tLogRes(res, err, start - end)\n\n return res, err\n}\n\nclient := SDK_PackageName.SDK_ClientInitializerName(\n\tSDK_PackageOptionName.WithMiddleware(Logger),\n)\n```\n\nWhen multiple middlewares are provided as variadic arguments, the middlewares\nare applied left to right. If `SDK_PackageOptionName.WithMiddleware` is given\nmultiple times, for example first in the client then the method, the\nmiddleware in the client will run first and the middleware given in the method\nwill run next.\n\nYou may also replace the default `http.Client` with\n`SDK_PackageOptionName.WithHTTPClient(client)`. Only one http client is\naccepted (this overwrites any previous client) and receives requests after any\nmiddleware has been applied.\n\n## Semantic versioning\n\nThis package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:\n\n1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_\n2. Changes that we do not expect to impact the vast majority of users in practice.\n\nWe take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.\n\nWe are keen for your feedback; please open an [issue](https://www.github.com/linq-team/linq-go/issues) with questions, bugs, or suggestions.\n\n## Contributing\n\nSee [the contributing documentation](./CONTRIBUTING.md).\n', + '# Linq API V3 Go API Library\n\nGo Reference\n\nThe Linq API V3 Go library provides convenient access to the [Linq API V3 REST API](https://docs.linqapp.com)\nfrom applications written in Go.\n\nIt is generated with [Stainless](https://www.stainless.com/).\n\n## MCP Server\n\nUse the Linq API V3 MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application.\n\n[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40linqapp%2Fsdk-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaW5xYXBwL3Nkay1tY3AiXSwiZW52Ijp7IkxJTlFfQVBJX1YzX0FQSV9LRVkiOiJNeSBBUEkgS2V5IiwiTElOUV9XRUJIT09LX1NFQ1JFVCI6Ik15IFdlYmhvb2sgU2VjcmV0In19)\n[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40linqapp%2Fsdk-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40linqapp%2Fsdk-mcp%22%5D%2C%22env%22%3A%7B%22LINQ_API_V3_API_KEY%22%3A%22My%20API%20Key%22%2C%22LINQ_WEBHOOK_SECRET%22%3A%22My%20Webhook%20Secret%22%7D%7D)\n\n> Note: You may need to set environment variables in your MCP client.\n\n## Installation\n\n\n\n```go\nimport (\n\t"github.com/linq-team/linq-go" // imported as SDK_PackageName\n)\n```\n\n\n\nOr to pin the version:\n\n\n\n```sh\ngo get -u \'github.com/linq-team/linq-go@v0.29.0\'\n```\n\n\n\n## Requirements\n\nThis library requires Go 1.22+.\n\n## Usage\n\nThe full API of this library can be found in [api.md](api.md).\n\n```go\npackage main\n\nimport (\n\t"context"\n\t"fmt"\n\n\t"github.com/linq-team/linq-go"\n\t"github.com/linq-team/linq-go/option"\n)\n\nfunc main() {\n\tclient := linqgo.NewClient(\n\t\toption.WithAPIKey("My API Key"), // defaults to os.LookupEnv("LINQ_API_V3_API_KEY")\n\t)\n\tchat, err := client.Chats.New(context.TODO(), linqgo.ChatNewParams{\n\t\tFrom: "+12052535597",\n\t\tMessage: linqgo.MessageContentParam{},\n\t\tTo: []string{"+12052532136"},\n\t})\n\tif err != nil {\n\t\tpanic(err.Error())\n\t}\n\tfmt.Printf("%+v\\n", chat.Chat)\n}\n\n```\n\n### Request fields\n\nAll request parameters are wrapped in a generic `Field` type,\nwhich we use to distinguish zero values from null or omitted fields.\n\nThis prevents accidentally sending a zero value if you forget a required parameter,\nand enables explicitly sending `null`, `false`, `\'\'`, or `0` on optional parameters.\nAny field not specified is not sent.\n\nTo construct fields with values, use the helpers `String()`, `Int()`, `Float()`, or most commonly, the generic `F[T]()`.\nTo send a null, use `Null[T]()`, and to send a nonconforming value, use `Raw[T](any)`. For example:\n\n```go\nparams := FooParams{\n\tName: SDK_PackageName.F("hello"),\n\n\t// Explicitly send `"description": null`\n\tDescription: SDK_PackageName.Null[string](),\n\n\tPoint: SDK_PackageName.F(SDK_PackageName.Point{\n\t\tX: SDK_PackageName.Int(0),\n\t\tY: SDK_PackageName.Int(1),\n\n\t\t// In cases where the API specifies a given type,\n\t\t// but you want to send something else, use `Raw`:\n\t\tZ: SDK_PackageName.Raw[int64](0.01), // sends a float\n\t}),\n}\n```\n\n### Response objects\n\nAll fields in response structs are value types (not pointers or wrappers).\n\nIf a given field is `null`, not present, or invalid, the corresponding field\nwill simply be its zero value.\n\nAll response structs also include a special `JSON` field, containing more detailed\ninformation about each property, which you can use like so:\n\n```go\nif res.Name == "" {\n\t// true if `"name"` is either not present or explicitly null\n\tres.JSON.Name.IsNull()\n\n\t// true if the `"name"` key was not present in the response JSON at all\n\tres.JSON.Name.IsMissing()\n\n\t// When the API returns data that cannot be coerced to the expected type:\n\tif res.JSON.Name.IsInvalid() {\n\t\traw := res.JSON.Name.Raw()\n\n\t\tlegacyName := struct{\n\t\t\tFirst string `json:"first"`\n\t\t\tLast string `json:"last"`\n\t\t}{}\n\t\tjson.Unmarshal([]byte(raw), &legacyName)\n\t\tname = legacyName.First + " " + legacyName.Last\n\t}\n}\n```\n\nThese `.JSON` structs also include an `Extras` map containing\nany properties in the json response that were not specified\nin the struct. This can be useful for API features not yet\npresent in the SDK.\n\n```go\nbody := res.JSON.ExtraFields["my_unexpected_field"].Raw()\n```\n\n### RequestOptions\n\nThis library uses the functional options pattern. Functions defined in the\n`SDK_PackageOptionName` package return a `RequestOption`, which is a closure that mutates a\n`RequestConfig`. These options can be supplied to the client or at individual\nrequests. For example:\n\n```go\nclient := SDK_PackageName.SDK_ClientInitializerName(\n\t// Adds a header to every request made by the client\n\tSDK_PackageOptionName.WithHeader("X-Some-Header", "custom_header_info"),\n)\n\nclient.Chats.New(context.TODO(), ...,\n\t// Override the header\n\tSDK_PackageOptionName.WithHeader("X-Some-Header", "some_other_custom_header_info"),\n\t// Add an undocumented field to the request body, using sjson syntax\n\tSDK_PackageOptionName.WithJSONSet("some.json.path", map[string]string{"my": "object"}),\n)\n```\n\nSee the [full list of request options](https://pkg.go.dev/github.com/linq-team/linq-go/SDK_PackageOptionName).\n\n### Pagination\n\nThis library provides some conveniences for working with paginated list endpoints.\n\nYou can use `.ListAutoPaging()` methods to iterate through items across all pages:\n\n```go\niter := client.Chats.ListChatsAutoPaging(context.TODO(), linqgo.ChatListChatsParams{})\n// Automatically fetches more pages as needed.\nfor iter.Next() {\n\tchat := iter.Current()\n\tfmt.Printf("%+v\\n", chat)\n}\nif err := iter.Err(); err != nil {\n\tpanic(err.Error())\n}\n```\n\nOr you can use simple `.List()` methods to fetch a single page and receive a standard response object\nwith additional helper methods like `.GetNextPage()`, e.g.:\n\n```go\npage, err := client.Chats.ListChats(context.TODO(), linqgo.ChatListChatsParams{})\nfor page != nil {\n\tfor _, chat := range page.Chats {\n\t\tfmt.Printf("%+v\\n", chat)\n\t}\n\tpage, err = page.GetNextPage()\n}\nif err != nil {\n\tpanic(err.Error())\n}\n```\n\n### Errors\n\nWhen the API returns a non-success status code, we return an error with type\n`*SDK_PackageName.Error`. This contains the `StatusCode`, `*http.Request`, and\n`*http.Response` values of the request, as well as the JSON of the error body\n(much like other response objects in the SDK).\n\nTo handle errors, we recommend that you use the `errors.As` pattern:\n\n```go\n_, err := client.Chats.New(context.TODO(), linqgo.ChatNewParams{\n\tFrom: "+12052535597",\n\tMessage: linqgo.MessageContentParam{},\n\tTo: []string{"+12052532136"},\n})\nif err != nil {\n\tvar apierr *linqgo.Error\n\tif errors.As(err, &apierr) {\n\t\tprintln(string(apierr.DumpRequest(true))) // Prints the serialized HTTP request\n\t\tprintln(string(apierr.DumpResponse(true))) // Prints the serialized HTTP response\n\t}\n\tpanic(err.Error()) // GET "/v3/chats": 400 Bad Request { ... }\n}\n```\n\nWhen other errors occur, they are returned unwrapped; for example,\nif HTTP transport fails, you might receive `*url.Error` wrapping `*net.OpError`.\n\n### Timeouts\n\nRequests do not time out by default; use context to configure a timeout for a request lifecycle.\n\nNote that if a request is [retried](#retries), the context timeout does not start over.\nTo set a per-retry timeout, use `SDK_PackageOptionName.WithRequestTimeout()`.\n\n```go\n// This sets the timeout for the request, including all the retries.\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)\ndefer cancel()\nclient.Chats.New(\n\tctx,\n\tlinqgo.ChatNewParams{\n\t\tFrom: "+12052535597",\n\t\tMessage: linqgo.MessageContentParam{},\n\t\tTo: []string{"+12052532136"},\n\t},\n\t// This sets the per-retry timeout\n\toption.WithRequestTimeout(20*time.Second),\n)\n```\n\n### File uploads\n\nRequest parameters that correspond to file uploads in multipart requests are typed as\n`param.Field[io.Reader]`. The contents of the `io.Reader` will by default be sent as a multipart form\npart with the file name of "anonymous_file" and content-type of "application/octet-stream".\n\nThe file name and content-type can be customized by implementing `Name() string` or `ContentType()\nstring` on the run-time type of `io.Reader`. Note that `os.File` implements `Name() string`, so a\nfile returned by `os.Open` will be sent with the file name on disk.\n\nWe also provide a helper `SDK_PackageName.FileParam(reader io.Reader, filename string, contentType string)`\nwhich can be used to wrap any `io.Reader` with the appropriate file name and content type.\n\n\n\n### Retries\n\nCertain errors will be automatically retried 2 times by default, with a short exponential backoff.\nWe retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit,\nand >=500 Internal errors.\n\nYou can use the `WithMaxRetries` option to configure or disable this:\n\n```go\n// Configure the default for all requests:\nclient := linqgo.NewClient(\n\toption.WithMaxRetries(0), // default is 2\n)\n\n// Override per-request:\nclient.Chats.New(\n\tcontext.TODO(),\n\tlinqgo.ChatNewParams{\n\t\tFrom: "+12052535597",\n\t\tMessage: linqgo.MessageContentParam{},\n\t\tTo: []string{"+12052532136"},\n\t},\n\toption.WithMaxRetries(5),\n)\n```\n\n\n### Accessing raw response data (e.g. response headers)\n\nYou can access the raw HTTP response data by using the `option.WithResponseInto()` request option. This is useful when\nyou need to examine response headers, status codes, or other details.\n\n```go\n// Create a variable to store the HTTP response\nvar response *http.Response\nchat, err := client.Chats.New(\n\tcontext.TODO(),\n\tlinqgo.ChatNewParams{\n\t\tFrom: "+12052535597",\n\t\tMessage: linqgo.MessageContentParam{},\n\t\tTo: []string{"+12052532136"},\n\t},\n\toption.WithResponseInto(&response),\n)\nif err != nil {\n\t// handle error\n}\nfmt.Printf("%+v\\n", chat)\n\nfmt.Printf("Status Code: %d\\n", response.StatusCode)\nfmt.Printf("Headers: %+#v\\n", response.Header)\n```\n\n### Making custom/undocumented requests\n\nThis library is typed for convenient access to the documented API. If you need to access undocumented\nendpoints, params, or response properties, the library can still be used.\n\n#### Undocumented endpoints\n\nTo make requests to undocumented endpoints, you can use `client.Get`, `client.Post`, and other HTTP verbs.\n`RequestOptions` on the client, such as retries, will be respected when making these requests.\n\n```go\nvar (\n // params can be an io.Reader, a []byte, an encoding/json serializable object,\n // or a "…Params" struct defined in this library.\n params map[string]interface{}\n\n // result can be an []byte, *http.Response, a encoding/json deserializable object,\n // or a model defined in this library.\n result *http.Response\n)\nerr := client.Post(context.Background(), "/unspecified", params, &result)\nif err != nil {\n …\n}\n```\n\n#### Undocumented request params\n\nTo make requests using undocumented parameters, you may use either the `SDK_PackageOptionName.WithQuerySet()`\nor the `SDK_PackageOptionName.WithJSONSet()` methods.\n\n```go\nparams := FooNewParams{\n ID: SDK_PackageName.F("id_xxxx"),\n Data: SDK_PackageName.F(FooNewParamsData{\n FirstName: SDK_PackageName.F("John"),\n }),\n}\nclient.Foo.New(context.Background(), params, SDK_PackageOptionName.WithJSONSet("data.last_name", "Doe"))\n```\n\n#### Undocumented response properties\n\nTo access undocumented response properties, you may either access the raw JSON of the response as a string\nwith `result.JSON.RawJSON()`, or get the raw JSON of a particular field on the result with\n`result.JSON.Foo.Raw()`.\n\nAny fields that are not present on the response struct will be saved and can be accessed by `result.JSON.ExtraFields()` which returns the extra fields as a `map[string]Field`.\n\n### Middleware\n\nWe provide `SDK_PackageOptionName.WithMiddleware` which applies the given\nmiddleware to requests.\n\n```go\nfunc Logger(req *http.Request, next SDK_PackageOptionName.MiddlewareNext) (res *http.Response, err error) {\n\t// Before the request\n\tstart := time.Now()\n\tLogReq(req)\n\n\t// Forward the request to the next handler\n\tres, err = next(req)\n\n\t// Handle stuff after the request\n\tend := time.Now()\n\tLogRes(res, err, start - end)\n\n return res, err\n}\n\nclient := SDK_PackageName.SDK_ClientInitializerName(\n\tSDK_PackageOptionName.WithMiddleware(Logger),\n)\n```\n\nWhen multiple middlewares are provided as variadic arguments, the middlewares\nare applied left to right. If `SDK_PackageOptionName.WithMiddleware` is given\nmultiple times, for example first in the client then the method, the\nmiddleware in the client will run first and the middleware given in the method\nwill run next.\n\nYou may also replace the default `http.Client` with\n`SDK_PackageOptionName.WithHTTPClient(client)`. Only one http client is\naccepted (this overwrites any previous client) and receives requests after any\nmiddleware has been applied.\n\n## Semantic versioning\n\nThis package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:\n\n1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_\n2. Changes that we do not expect to impact the vast majority of users in practice.\n\nWe take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.\n\nWe are keen for your feedback; please open an [issue](https://www.github.com/linq-team/linq-go/issues) with questions, bugs, or suggestions.\n\n## Contributing\n\nSee [the contributing documentation](./CONTRIBUTING.md).\n', }, { language: 'python', diff --git a/packages/mcp-server/src/methods.ts b/packages/mcp-server/src/methods.ts index ede02b4..7dd3ff1 100644 --- a/packages/mcp-server/src/methods.ts +++ b/packages/mcp-server/src/methods.ts @@ -106,6 +106,12 @@ export const sdkMethods: SdkMethod[] = [ httpMethod: 'post', httpPath: '/v3/chats/{chatId}/location/request', }, + { + clientCallName: 'client.chats.polls.create', + fullyQualifiedName: 'chats.polls.create', + httpMethod: 'post', + httpPath: '/v3/chats/{chatId}/polls', + }, { clientCallName: 'client.messages.create', fullyQualifiedName: 'messages.create', @@ -148,6 +154,24 @@ export const sdkMethods: SdkMethod[] = [ httpMethod: 'post', httpPath: '/v3/messages/{messageId}/update', }, + { + clientCallName: 'client.messages.poll.retrieve', + fullyQualifiedName: 'messages.poll.retrieve', + httpMethod: 'get', + httpPath: '/v3/messages/{messageId}/poll', + }, + { + clientCallName: 'client.messages.poll.addOptions', + fullyQualifiedName: 'messages.poll.addOptions', + httpMethod: 'post', + httpPath: '/v3/messages/{messageId}/poll/options', + }, + { + clientCallName: 'client.messages.poll.vote', + fullyQualifiedName: 'messages.poll.vote', + httpMethod: 'post', + httpPath: '/v3/messages/{messageId}/poll/votes', + }, { clientCallName: 'client.attachments.create', fullyQualifiedName: 'attachments.create', diff --git a/packages/mcp-server/src/server.ts b/packages/mcp-server/src/server.ts index fc8e1c6..71df70a 100644 --- a/packages/mcp-server/src/server.ts +++ b/packages/mcp-server/src/server.ts @@ -28,7 +28,7 @@ export const newMcpServer = async ({ new McpServer( { name: 'linqapp_sdk_api', - version: '0.32.1', + version: '0.33.0', }, { instructions: await getInstructions({ stainlessApiKey, customInstructionsPath }), diff --git a/src/client.ts b/src/client.ts index f620260..c4bdd59 100644 --- a/src/client.ts +++ b/src/client.ts @@ -53,21 +53,6 @@ import { SetContactCard, } from './resources/contact-card'; import { ExperienceListResponse, ExperienceRetrieveResponse, Experiences } from './resources/experiences'; -import { - Message, - MessageAddReactionParams, - MessageAddReactionResponse, - MessageCreateParams, - MessageCreateResponse, - MessageEffect, - MessageListMessagesThreadParams, - MessageUpdateAppCardParams, - MessageUpdateAppCardResponse, - MessageUpdateParams, - Messages, - MessagesListMessagesPagination, - ReplyTo, -} from './resources/messages'; import { PaymentHandleConnection, PaymentHandleVerifyParams, @@ -148,6 +133,21 @@ import { MessageContent, TextPart, } from './resources/chats/chats'; +import { + Message, + MessageAddReactionParams, + MessageAddReactionResponse, + MessageCreateParams, + MessageCreateResponse, + MessageEffect, + MessageListMessagesThreadParams, + MessageUpdateAppCardParams, + MessageUpdateAppCardResponse, + MessageUpdateParams, + Messages, + MessagesListMessagesPagination, + ReplyTo, +} from './resources/messages/messages'; import { type Fetch } from './internal/builtin-types'; import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers'; import { FinalRequestOptions, RequestOptions } from './internal/request-options'; diff --git a/src/core/error.ts b/src/core/error.ts index 6c347bf..b8e2535 100644 --- a/src/core/error.ts +++ b/src/core/error.ts @@ -16,11 +16,30 @@ export class APIError< /** JSON body of the response that caused the error */ readonly error: TError; + /** + * Linq API error code. + */ + readonly code: number | undefined; + /** + * Link to documentation for this error code + */ + readonly doc_url: string | undefined; + /** + * Number of seconds to wait before retrying. Only present on 429 rate limit + * errors. + */ + readonly retry_after?: number | undefined; + constructor(status: TStatus, error: TError, message: string | undefined, headers: THeaders) { super(`${APIError.makeMessage(status, error, message)}`); this.status = status; this.headers = headers; this.error = error; + + const data = error as Record; + this.code = data?.['code']; + this.doc_url = data?.['doc_url']; + this.retry_after = data?.['retry_after']; } private static makeMessage(status: number | undefined, error: any, message: string | undefined) { @@ -54,7 +73,7 @@ export class APIError< return new APIConnectionError({ message, cause: castToError(errorResponse) }); } - const error = errorResponse as Record; + const error = (errorResponse as Record)?.['error']; if (status === 400) { return new BadRequestError(status, error, message, headers); diff --git a/src/resources/chats/chats.ts b/src/resources/chats/chats.ts index 623eec5..e91117a 100644 --- a/src/resources/chats/chats.ts +++ b/src/resources/chats/chats.ts @@ -1,11 +1,10 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../../core/resource'; -import * as MessagesAPI from '../messages'; import * as Shared from '../shared'; import * as LocationAPI from './location'; import { GetChatLocationResponse, Location, LocationRequestResponse } from './location'; -import * as ChatsMessagesAPI from './messages'; +import * as MessagesAPI from './messages'; import { MessageListParams, MessageSendParams, MessageSendResponse, Messages, SentMessage } from './messages'; import * as ParticipantsAPI from './participants'; import { @@ -15,8 +14,11 @@ import { ParticipantRemoveResponse, Participants, } from './participants'; +import * as PollsAPI from './polls'; +import { Poll, PollCreateParams, PollEnvelope, Polls } from './polls'; import * as TypingAPI from './typing'; import { Typing } from './typing'; +import * as ResourcesMessagesAPI from '../messages/messages'; import { APIPromise } from '../../core/api-promise'; import { ListChatsPagination, type ListChatsPaginationParams, PagePromise } from '../../core/pagination'; import { buildHeaders } from '../../internal/headers'; @@ -26,8 +28,9 @@ import { path } from '../../internal/utils/path'; export class Chats extends APIResource { participants: ParticipantsAPI.Participants = new ParticipantsAPI.Participants(this._client); typing: TypingAPI.Typing = new TypingAPI.Typing(this._client); - messages: ChatsMessagesAPI.Messages = new ChatsMessagesAPI.Messages(this._client); + messages: MessagesAPI.Messages = new MessagesAPI.Messages(this._client); location: LocationAPI.Location = new LocationAPI.Location(this._client); + polls: PollsAPI.Polls = new PollsAPI.Polls(this._client); /** * Create a new chat with specified participants and send an initial message. The @@ -385,15 +388,19 @@ export namespace Chat { * to react. `doc_url` deep-links to the relevant section. * * `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - * `CANCEL`, `END`, or `QUIT`, and you should send nothing further on this chat. - * The keyword must be the whole trimmed message, never part of a longer one: - * `STOP` counts, `please stop` does not. Most keywords must match exactly, - * including case. `OPT OUT` is the exception — it matches in any casing, with or - * without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. - * It clears if they later send `START`, `OPTIN`, or `UNSTOP`, or if they keep - * replying on the chat — sustained two-way conversation is treated as a sign the - * stop keyword was a false positive. Suppressing sends to opted-out recipients is - * your responsibility — Linq surfaces the status but does not block the send. + * `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never + * part of a longer one: `STOP` counts, `please stop` does not. Most keywords must + * match exactly, including case. `OPT OUT` is the exception — it matches in any + * casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and + * `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, + * or if they keep replying on the chat — sustained two-way conversation is treated + * as a sign the stop keyword was a false positive. + * + * Linq enforces this: while a recipient is opted out, every send to them is + * rejected with `403` (error code `2024`) before the message is queued, across + * every chat and every line on your account. Nothing is delivered, including a + * final courtesy message — to send one, set `override_optout: true` on that single + * request. */ status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; @@ -492,7 +499,7 @@ export interface MessageContent { /** * iMessage effect to apply to this message (screen or bubble effect) */ - effect?: MessagesAPI.MessageEffect; + effect?: ResourcesMessagesAPI.MessageEffect; /** * Optional idempotency key for this message. Use this to prevent duplicate sends @@ -551,7 +558,7 @@ export interface MessageContent { /** * Reply to another message to create a threaded conversation */ - reply_to?: MessagesAPI.ReplyTo; + reply_to?: ResourcesMessagesAPI.ReplyTo; } export namespace MessageContent { @@ -744,6 +751,26 @@ export interface TextPart { */ value: string; + /** + * @mention a chat participant (iMessage group chats only). Set to their handle — + * E.164 phone number or Apple ID email. `value` is the display text; use the bare + * name (`"Juan"`, not `"@Juan"`). The mentioned participant is notified even if + * the chat is muted. Falls back to plain text over SMS/RCS. + * + * By default the entire `value` renders as the mention; use `mention_range` to + * highlight only part of it. + */ + mention?: string; + + /** + * Optional character range `[start, end)` in `value` that renders as the `mention` + * highlight (e.g. just the name in `"Hey Kevin, can you look at this?"`). Requires + * `mention`. Without it, the entire `value` is highlighted. `start` is inclusive, + * `end` is exclusive. _Characters are measured as UTF-16 code units. Most + * characters count as 1; some emoji count as 2._ + */ + mention_range?: Array; + /** * Optional array of text decorations applied to character ranges in the `value` * field (iMessage only). @@ -815,7 +842,7 @@ export namespace ChatCreateResponse { /** * A message that was sent (used in CreateChat and SendMessage responses) */ - message: ChatsMessagesAPI.SentMessage; + message: MessagesAPI.SentMessage; /** * Messaging service type @@ -849,15 +876,19 @@ export namespace ChatCreateResponse { * to react. `doc_url` deep-links to the relevant section. * * `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - * `CANCEL`, `END`, or `QUIT`, and you should send nothing further on this chat. - * The keyword must be the whole trimmed message, never part of a longer one: - * `STOP` counts, `please stop` does not. Most keywords must match exactly, - * including case. `OPT OUT` is the exception — it matches in any casing, with or - * without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. - * It clears if they later send `START`, `OPTIN`, or `UNSTOP`, or if they keep - * replying on the chat — sustained two-way conversation is treated as a sign the - * stop keyword was a false positive. Suppressing sends to opted-out recipients is - * your responsibility — Linq surfaces the status but does not block the send. + * `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never + * part of a longer one: `STOP` counts, `please stop` does not. Most keywords must + * match exactly, including case. `OPT OUT` is the exception — it matches in any + * casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and + * `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, + * or if they keep replying on the chat — sustained two-way conversation is treated + * as a sign the stop keyword was a false positive. + * + * Linq enforces this: while a recipient is opted out, every send to them is + * rejected with `403` (error code `2024`) before the message is queued, across + * every chat and every line on your account. Nothing is delivered, including a + * final courtesy message — to send one, set `override_optout: true` on that single + * request. */ status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; @@ -1013,6 +1044,14 @@ export interface ChatCreateParams { * For individual chats, provide one recipient. For group chats, provide multiple. */ to: Array; + + /** + * Send even though the recipient asked you to stop (`403`, error code `2024`). + * Applies to this request only: the opt-out stays in place, so the next send + * without this flag is rejected again. Every override is recorded against your API + * key. + */ + override_optout?: boolean; } export interface ChatUpdateParams { @@ -1054,6 +1093,14 @@ export interface ChatSendVoicememoParams { */ attachment_id?: string; + /** + * Send even though the recipient asked you to stop (`403`, error code `2024`). + * Applies to this request only: the opt-out stays in place, so the next send + * without this flag is rejected again. Every override is recorded against your API + * key. + */ + override_optout?: boolean; + /** * URL of the voice memo audio file. Must be a publicly accessible HTTPS URL. * @@ -1066,6 +1113,7 @@ Chats.Participants = Participants; Chats.Typing = Typing; Chats.Messages = Messages; Chats.Location = Location; +Chats.Polls = Polls; export declare namespace Chats { export { @@ -1108,4 +1156,11 @@ export declare namespace Chats { type GetChatLocationResponse as GetChatLocationResponse, type LocationRequestResponse as LocationRequestResponse, }; + + export { + Polls as Polls, + type Poll as Poll, + type PollEnvelope as PollEnvelope, + type PollCreateParams as PollCreateParams, + }; } diff --git a/src/resources/chats/index.ts b/src/resources/chats/index.ts index 7d60f97..6f5e09e 100644 --- a/src/resources/chats/index.ts +++ b/src/resources/chats/index.ts @@ -32,4 +32,5 @@ export { type ParticipantAddParams, type ParticipantRemoveParams, } from './participants'; +export { Polls, type Poll, type PollEnvelope, type PollCreateParams } from './polls'; export { Typing } from './typing'; diff --git a/src/resources/chats/messages.ts b/src/resources/chats/messages.ts index 3ef9dcf..97e0039 100644 --- a/src/resources/chats/messages.ts +++ b/src/resources/chats/messages.ts @@ -1,10 +1,10 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. import { APIResource } from '../../core/resource'; -import * as MessagesAPI from '../messages'; -import { MessagesListMessagesPagination } from '../messages'; import * as Shared from '../shared'; import * as ChatsAPI from './chats'; +import * as ResourcesMessagesAPI from '../messages/messages'; +import { MessagesListMessagesPagination } from '../messages/messages'; import { APIPromise } from '../../core/api-promise'; import { ListMessagesPagination, @@ -92,10 +92,10 @@ export class Messages extends APIResource { chatID: string, query: MessageListParams | null | undefined = {}, options?: RequestOptions, - ): PagePromise { + ): PagePromise { return this._client.getAPIList( path`/v3/chats/${chatID}/messages`, - ListMessagesPagination, + ListMessagesPagination, { query, ...options }, ); } @@ -209,7 +209,7 @@ export interface SentMessage { /** * iMessage effect applied to a message (screen or bubble effect) */ - effect?: MessagesAPI.MessageEffect | null; + effect?: ResourcesMessagesAPI.MessageEffect | null; /** * The sender of this message as a full handle object @@ -224,7 +224,7 @@ export interface SentMessage { /** * Indicates this message is a threaded reply to another message */ - reply_to?: MessagesAPI.ReplyTo | null; + reply_to?: ResourcesMessagesAPI.ReplyTo | null; /** * Messaging service type @@ -398,6 +398,14 @@ export interface MessageSendParams { * cannot coexist with text), so copy and a card are two sends, not one. */ message: ChatsAPI.MessageContent; + + /** + * Send even though the recipient asked you to stop (`403`, error code `2024`). + * Applies to this request only: the opt-out stays in place, so the next send + * without this flag is rejected again. Every override is recorded against your API + * key. + */ + override_optout?: boolean; } export declare namespace Messages { diff --git a/src/resources/chats/polls.ts b/src/resources/chats/polls.ts new file mode 100644 index 0000000..a366e2e --- /dev/null +++ b/src/resources/chats/polls.ts @@ -0,0 +1,196 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../core/resource'; +import * as Shared from '../shared'; +import { APIPromise } from '../../core/api-promise'; +import { RequestOptions } from '../../internal/request-options'; +import { path } from '../../internal/utils/path'; + +/** + * Messages are individual communications within a chat thread. + * + * Messages can include text, media attachments, rich link previews, special effects + * (like confetti or fireworks), and reactions. All messages are associated with a + * specific chat and sent from a phone number you own. + * + * Messages support delivery status tracking, read receipts, and editing capabilities. + * + * ## Rich Link Previews + * + * Send a URL as a `link` part to deliver it with a rich preview card showing the + * page's title, description, and image (when available). A `link` part must be the + * **only** part in the message — it cannot be combined with text or media parts. + * To send a URL without a preview card, include it in a `text` part instead. + * + * **Limitations:** + * - A `link` part cannot be combined with other parts in the same message. + * - Maximum URL length: 2,048 characters. + * + * ## Ephemeral Messages (Privacy Tier) + * + * For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + * + * You can request it at two scopes: + * + * | Scope | Effect | + * |---|---| + * | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + * | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + * + * **Behavioral differences vs the standard default:** + * + * | Aspect | Standard | Ephemeral | + * |---|---|---| + * | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + * | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + * | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + * | Cross-partner isolation | Enforced | Enforced | + * + * **How the 24-hour window works:** + * + * - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + * - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + * - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + * + * **What you observe:** + * + * - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + * - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + * - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + * + * **When to choose ephemeral:** + * + * - You have a compliance requirement that the platform must not retain message content beyond a short window. + * - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + * - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + * + * **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. + */ +export class Polls extends APIResource { + /** + * Create an iMessage poll in an existing chat and send it. Polls are + * iMessage-only. + * + * The chat must already exist — **a poll cannot be the first message of a new + * chat** (use `POST /v3/chats` for that). Options are **add-only and immutable**: + * you can add options later via `POST /v3/messages/{messageId}/poll/options`, but + * never edit or remove them. + * + * @example + * ```ts + * const pollEnvelope = await client.chats.polls.create( + * '550e8400-e29b-41d4-a716-446655440000', + * { + * poll: { + * options: [{ text: 'Tacos' }, { text: 'Sushi' }], + * idempotency_key: 'poll-abc123', + * }, + * }, + * ); + * ``` + */ + create(chatID: string, body: PollCreateParams, options?: RequestOptions): APIPromise { + return this._client.post(path`/v3/chats/${chatID}/polls`, { body, ...options }); + } +} + +/** + * Poll content — options and the aggregate voter count. + */ +export interface Poll { + options: Array; + + /** + * Distinct participants across the whole poll (a voter picking two options counts + * once). + */ + total_voters: number; +} + +export namespace Poll { + export interface Option { + can_be_edited: boolean; + + /** + * The participant who added this option (poll creator for the initial options; + * whoever added later ones). + */ + creator_handle: Shared.ChatHandle; + + option_id: string; + + text: string; + + /** + * Participants who voted for this option (vote_count = voters.length). + */ + voters: Array; + } + + export namespace Option { + export interface Voter { + handle: string; + + voted_at: string; + } + } +} + +/** + * Message-level envelope returned by every poll endpoint. + */ +export interface PollEnvelope { + chat_id: string; + + created_at: string; + + /** + * The poll-definition message's ID — reference this poll by it. + */ + message_id: string; + + /** + * Poll content — options and the aggregate voter count. + */ + poll: Poll; + + /** + * Tapbacks/stickers on the whole poll (message part 0). + */ + reactions: Array; + + updated_at: string; +} + +export interface PollCreateParams { + /** + * Poll content to create. A poll needs at least two options. Options are add-only + * and immutable — there is no title/question (send that as a normal text message). + */ + poll: PollCreateParams.Poll; +} + +export namespace PollCreateParams { + /** + * Poll content to create. A poll needs at least two options. Options are add-only + * and immutable — there is no title/question (send that as a normal text message). + */ + export interface Poll { + options: Array; + + /** + * Optional key to deduplicate the poll creation. + */ + idempotency_key?: string; + } + + export namespace Poll { + export interface Option { + text: string; + } + } +} + +export declare namespace Polls { + export { type Poll as Poll, type PollEnvelope as PollEnvelope, type PollCreateParams as PollCreateParams }; +} diff --git a/src/resources/index.ts b/src/resources/index.ts index 573433f..c1c2daf 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -60,7 +60,7 @@ export { type MessageListMessagesThreadParams, type MessageUpdateAppCardParams, type MessagesListMessagesPagination, -} from './messages'; +} from './messages/messages'; export { PaymentHandles, type PaymentHandleConnection, diff --git a/src/resources/messages.ts b/src/resources/messages.ts index e4ba9e2..eb17523 100644 --- a/src/resources/messages.ts +++ b/src/resources/messages.ts @@ -1,934 +1,3 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { APIResource } from '../core/resource'; -import * as Shared from './shared'; -import * as ChatsAPI from './chats/chats'; -import * as ChatsMessagesAPI from './chats/messages'; -import { APIPromise } from '../core/api-promise'; -import { ListMessagesPagination, type ListMessagesPaginationParams, PagePromise } from '../core/pagination'; -import { buildHeaders } from '../internal/headers'; -import { RequestOptions } from '../internal/request-options'; -import { path } from '../internal/utils/path'; - -/** - * Messages are individual communications within a chat thread. - * - * Messages can include text, media attachments, rich link previews, special effects - * (like confetti or fireworks), and reactions. All messages are associated with a - * specific chat and sent from a phone number you own. - * - * Messages support delivery status tracking, read receipts, and editing capabilities. - * - * ## Rich Link Previews - * - * Send a URL as a `link` part to deliver it with a rich preview card showing the - * page's title, description, and image (when available). A `link` part must be the - * **only** part in the message — it cannot be combined with text or media parts. - * To send a URL without a preview card, include it in a `text` part instead. - * - * **Limitations:** - * - A `link` part cannot be combined with other parts in the same message. - * - Maximum URL length: 2,048 characters. - * - * ## Ephemeral Messages (Privacy Tier) - * - * For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. - * - * You can request it at two scopes: - * - * | Scope | Effect | - * |---|---| - * | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | - * | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | - * - * **Behavioral differences vs the standard default:** - * - * | Aspect | Standard | Ephemeral | - * |---|---|---| - * | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | - * | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | - * | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | - * | Cross-partner isolation | Enforced | Enforced | - * - * **How the 24-hour window works:** - * - * - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. - * - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. - * - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. - * - * **What you observe:** - * - * - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. - * - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. - * - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. - * - * **When to choose ephemeral:** - * - * - You have a compliance requirement that the platform must not retain message content beyond a short window. - * - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. - * - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. - * - * **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. - */ -export class Messages extends APIResource { - /** - * Send a message to one or more recipients **without supplying a `from` number**. - * Linq resolves both the sending line and the target chat for you, then returns - * exactly which line was used, which chat the message landed in, whether a new - * chat was created, and every resulting message id. - * - * This fuses "create chat" and "send message" behind a single message-centric - * resource. Provide only the recipients (`to`) and the `message`; the platform - * decides the rest. - * - * ## How the from-number and chat are chosen - * - * - **Reuse** — if a chat with exactly these recipients already exists on a line - * that can still send, the message is sent into that chat on its existing line - * (`from_selection.reason = reused_active_chat`). The most-recently-active such - * chat wins; chats stranded on flagged lines (e.g. by an earlier failover) are - * skipped. - * - **New** — if no such chat exists, a new chat is created on the best available - * line (`from_selection.reason = new_best_number`). - * - **Failover** — if matching chats exist but none is on a line that can send, a - * **new** chat is created on a fresh best line and the flagged chat is abandoned - * (`from_selection.reason = failover_flagged`, `previous_chat_id` set). If you - * supply `continuation_message`, that text is sent as the single message INSTEAD - * of `message` (useful as a fresh-number-appropriate opener). Exactly one - * message is sent either way. - * - * Recipients (`to`) are an order-independent set: a single handle is a direct - * chat, multiple handles a group chat. - * - * ## Excluding lines - * - * `exclude_from` keeps specific lines out of **this** send's line pick. It only - * affects picking a line for a new chat — an existing chat is always reused on its - * own line, preferring a chat on a non-excluded line when the recipients have more - * than one. An exclusion never abandons a live chat or moves it to a new number, - * so if the only chat these recipients have is on an excluded line, that chat is - * still used. `from` tells you the line that was actually used. - * - * ## Differences from POST /v3/chats - * - * - The first message **may contain a link** (including for a newly created chat). - * Note: sending a link as the very first message on a freshly selected line can - * elevate that line's flagging risk — it is allowed, not recommended. - * - Voice memos are **not** supported here. To send an iMessage voice-memo bubble, - * use `POST /v3/chats/{chatId}/voicememo` with a known chat id. - * - * ## Service preference, effects, decorations - * - * Set `message.preferred_service` (`iMessage` | `RCS` | `SMS`), `message.effect`, - * and per-part `text_decorations` exactly as on the other send endpoints. - * - * Always responds `202 Accepted` — chat creation is incidental to the send. - * - * @example - * ```ts - * const message = await client.messages.create({ - * message: { - * parts: [ - * { - * type: 'text', - * value: - * 'Hi! Thanks for reaching out — how can we help?', - * }, - * ], - * }, - * to: ['+14155559876'], - * }); - * ``` - */ - create(params: MessageCreateParams, options?: RequestOptions): APIPromise { - const { 'Idempotency-Key': idempotencyKey, ...body } = params; - return this._client.post('/v3/messages', { - body, - ...options, - headers: buildHeaders([ - { ...(idempotencyKey != null ? { 'Idempotency-Key': idempotencyKey } : undefined) }, - options?.headers, - ]), - }); - } - - /** - * Retrieve a specific message by its ID. This endpoint returns the full message - * details including text, attachments, reactions, and metadata. - * - * @example - * ```ts - * const message = await client.messages.retrieve( - * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', - * ); - * ``` - */ - retrieve(messageID: string, options?: RequestOptions): APIPromise { - return this._client.get(path`/v3/messages/${messageID}`, options); - } - - /** - * Edit the text content of a specific part of a previously sent message. - * - * **Note:** A message can be edited up to 5 times, and only within 15 minutes of - * when it was originally sent. - * - * @example - * ```ts - * const message = await client.messages.update( - * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', - * { text: 'This is the edited message content' }, - * ); - * ``` - */ - update(messageID: string, body: MessageUpdateParams, options?: RequestOptions): APIPromise { - return this._client.patch(path`/v3/messages/${messageID}`, { body, ...options }); - } - - /** - * Deletes a message from the Linq API only. This does NOT unsend or remove the - * message from the actual chat — recipients will still see the message. Re-sending - * with a deleted message's idempotency key returns 404 — a deleted message is - * never resent. - * - * @example - * ```ts - * await client.messages.delete( - * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', - * ); - * ``` - */ - delete(messageID: string, options?: RequestOptions): APIPromise { - return this._client.delete(path`/v3/messages/${messageID}`, { - ...options, - headers: buildHeaders([{ Accept: '*/*' }, options?.headers]), - }); - } - - /** - * Add or remove emoji reactions to messages. Reactions let users express their - * response to a message without sending a new message. - * - * **Supported Reactions:** - * - * - love ❤️ - * - like 👍 - * - dislike 👎 - * - laugh 😂 - * - emphasize ‼️ - * - question ❓ - * - custom - any emoji (use `custom_emoji` field to specify) - * - * @example - * ```ts - * const response = await client.messages.addReaction( - * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', - * { operation: 'add', type: 'love' }, - * ); - * ``` - */ - addReaction( - messageID: string, - body: MessageAddReactionParams, - options?: RequestOptions, - ): APIPromise { - return this._client.post(path`/v3/messages/${messageID}/reactions`, { body, ...options }); - } - - /** - * Retrieve all messages in a conversation thread. Given any message ID in the - * thread, returns the originator message and all replies in chronological order. - * - * If the message is not part of a thread, returns just that single message. - * - * Supports pagination and configurable ordering. - * - * @example - * ```ts - * // Automatically fetches more pages as needed. - * for await (const message of client.messages.listMessagesThread( - * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', - * )) { - * // ... - * } - * ``` - */ - listMessagesThread( - messageID: string, - query: MessageListMessagesThreadParams | null | undefined = {}, - options?: RequestOptions, - ): PagePromise { - return this._client.getAPIList(path`/v3/messages/${messageID}/thread`, ListMessagesPagination, { - query, - ...options, - }); - } - - /** - * Replaces a previously delivered `imessage_app` card on the recipient's screen - * with new content, instead of posting a new bubble (like a game move redrawing - * the board). - * - * The update is delivered as a **new message** with its own id and delivery - * lifecycle (`message.sent` / `message.delivered` / `message.failed` webhooks fire - * for the new id). To update the card again, reference the message id returned by - * this call. - * - * Constraints: - * - * - The referenced message must be an `imessage_app` card sent by you (`400` - * otherwise — inbound cards cannot be updated). - * - The referenced card must already be delivered (`409` otherwise — retry after - * the `message.delivered` webhook for it). - * - The app identity (`team_id`, `bundle_id`, name) is inherited from the original - * card and cannot change; only `url`, `fallback_text`, and `layout` are - * replaced. - * - iMessage-only, like all app cards. - * - Concurrent updates against the same card are not serialized server-side; the - * last one delivered wins on the recipient's screen. Serialize updates by always - * referencing the message id returned by the previous call. - * - * @example - * ```ts - * const response = await client.messages.updateAppCard( - * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', - * { - * layout: { caption: 'Score: 2 – 1' }, - * fallback_text: 'Score update', - * url: 'https://app.example.com/card?game=7f3a&move=2', - * }, - * ); - * ``` - */ - updateAppCard( - messageID: string, - body: MessageUpdateAppCardParams, - options?: RequestOptions, - ): APIPromise { - return this._client.post(path`/v3/messages/${messageID}/update`, { body, ...options }); - } -} - -export type MessagesListMessagesPagination = ListMessagesPagination; - -export interface Message { - /** - * Unique identifier for the message - */ - id: string; - - /** - * ID of the chat this message belongs to - */ - chat_id: string; - - /** - * When the message was created - */ - created_at: string; - - /** - * Current delivery status of a message - */ - delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; - - /** - * @deprecated DEPRECATED: Use `delivery_status` instead (true when - * `delivery_status` is `delivered` or `read`). Whether the message has been - * delivered. - */ - is_delivered: boolean; - - /** - * Whether this message was sent by the authenticated user - */ - is_from_me: boolean; - - /** - * @deprecated DEPRECATED: Use `delivery_status == "read"` instead. Whether the - * message has been read. - */ - is_read: boolean; - - /** - * When the message was last updated - */ - updated_at: string; - - /** - * When the message was delivered - */ - delivered_at?: string | null; - - /** - * iMessage effect applied to a message (screen or bubble effect) - */ - effect?: MessageEffect | null; - - /** - * @deprecated DEPRECATED: Use from_handle instead. Phone number of the message - * sender. - */ - from?: string | null; - - /** - * The sender of this message as a full handle object - */ - from_handle?: Shared.ChatHandle | null; - - /** - * Message parts in order (text, media, and link) - */ - parts?: Array< - | Shared.TextPartResponse - | Shared.MediaPartResponse - | Shared.LinkPartResponse - | Message.IMessageAppPartResponse - > | null; - - /** - * Messaging service type - */ - preferred_service?: Shared.ServiceType | null; - - /** - * When the message was read - */ - read_at?: string | null; - - /** - * Present only when this message was recovered by reconciliation rather than - * delivered live, and set to the time of that recovery. The field is omitted - * entirely for normally-delivered messages, which is the overwhelming majority. - * When present, expect `sent_at` to be substantially earlier — the message is - * genuine but was ingested late, so it may not have appeared in earlier reads of - * this conversation. - */ - reconciled_at?: string; - - /** - * Indicates this message is a threaded reply to another message - */ - reply_to?: ReplyTo | null; - - /** - * When the message was sent - */ - sent_at?: string | null; - - /** - * Messaging service type - */ - service?: Shared.ServiceType | null; -} - -export namespace Message { - /** - * An iMessage app card part. - */ - export interface IMessageAppPartResponse { - /** - * Identifies the iMessage app (Messages app extension) that backs the card. - */ - app: IMessageAppPartResponse.App; - - /** - * Visible layout of the card. At least one of `caption`, `subcaption`, - * `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise - * the card renders as an empty bubble. - * - * `image_url` displays a preview image at the top of the card. The image renders - * on the recipient's card whether or not they have your app installed. The small - * icon beside the caption is the app's own icon and is not settable here. - * - * `* Note - requires a trusted chat w/ inbound activity` - * - * `image_title` and `image_subtitle` render as text overlaid on the image (title - * bold, subtitle beneath it). They only appear when `image_url` is set — without - * an image there is nothing to overlay — so setting either without `image_url` is - * rejected. - */ - layout: IMessageAppPartResponse.Layout; - - /** - * Reactions on this message part - */ - reactions: Array | null; - - /** - * Indicates this is an iMessage app card part. - */ - type: 'imessage_app'; - - /** - * The URL delivered to the iMessage app on tap. - */ - url: string; - - /** - * Fallback text for surfaces that cannot render the card. - */ - fallback_text?: string | null; - } - - export namespace IMessageAppPartResponse { - /** - * Identifies the iMessage app (Messages app extension) that backs the card. - */ - export interface App { - /** - * Bundle identifier of the Messages app extension. Must not contain `:`. - */ - bundle_id: string; - - /** - * Display name of the app, shown by Messages' fallback UI. - */ - name: string; - - /** - * The app's 10-character uppercase alphanumeric team identifier. - */ - team_id: string; - - /** - * The owning app's App Store id (optional). When set, recipients without the - * iMessage app installed see a "Get the app" affordance. - */ - app_store_id?: number; - } - - /** - * Visible layout of the card. At least one of `caption`, `subcaption`, - * `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise - * the card renders as an empty bubble. - * - * `image_url` displays a preview image at the top of the card. The image renders - * on the recipient's card whether or not they have your app installed. The small - * icon beside the caption is the app's own icon and is not settable here. - * - * `* Note - requires a trusted chat w/ inbound activity` - * - * `image_title` and `image_subtitle` render as text overlaid on the image (title - * bold, subtitle beneath it). They only appear when `image_url` is set — without - * an image there is nothing to overlay — so setting either without `image_url` is - * rejected. - */ - export interface Layout { - /** - * Primary label, top-left and bold. - */ - caption?: string; - - /** - * Text shown below `image_title`, overlaid on the card image. Requires - * `image_url`. - */ - image_subtitle?: string; - - /** - * Bold text overlaid on the card image. Requires `image_url` (rejected without - * it). - */ - image_title?: string; - - /** - * URL of an image (JPEG, PNG, HEIF, or WebP) to display as the card's preview - * image; an unreachable or non-image URL returns a validation error. Renders for - * all recipients regardless of whether they have the app. Note - requires a - * trusted chat w/ inbound activity. In responses, this is the re-hosted - * `cdn.linqapp.com` copy of the image you supplied, not your original URL. - */ - image_url?: string; - - /** - * Secondary label, below `caption` on the left. - */ - subcaption?: string; - - /** - * Label shown top-right. - */ - trailing_caption?: string; - - /** - * Label shown below `trailing_caption`, on the right. - */ - trailing_subcaption?: string; - } - } -} - -/** - * iMessage effect applied to a message (screen or bubble effect) - */ -export interface MessageEffect { - /** - * Name of the effect. Common values: - * - * - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, - * love, balloons, happy_birthday, echo, spotlight - * - Bubble effects: slam, loud, gentle, invisible - */ - name?: string; - - /** - * Type of effect - */ - type?: 'screen' | 'bubble'; -} - -/** - * Indicates this message is a threaded reply to another message - */ -export interface ReplyTo { - /** - * The ID of the message to reply to - */ - message_id: string; - - /** - * The specific message part to reply to (0-based index). Defaults to 0 (first - * part) if not provided. Use this when replying to a specific part of a multipart - * message. - */ - part_index?: number; -} - -/** - * Result of an auto-from send. Self-describing: which line was used, which chat - * the message landed in, whether a new chat was created, and the resulting message - * id(s). - */ -export interface MessageCreateResponse { - /** - * The resolved chat (reused or newly created) the message landed in. - */ - chat_id: string; - - /** - * True when a new chat was created (new or failover), false on reuse. - */ - created_new_chat: boolean; - - /** - * The line (E.164) the message was actually sent from. - */ - from: string; - - /** - * Why this line/chat was chosen. - */ - from_selection: MessageCreateResponse.FromSelection; - - /** - * Participants of the resolved chat. - */ - handles: Array; - - /** - * Whether the resolved chat is a group chat. - */ - is_group: boolean; - - /** - * A message that was sent (used in CreateChat and SendMessage responses) - */ - message: ChatsMessagesAPI.SentMessage; - - /** - * Messaging service type - */ - service: Shared.ServiceType; - - /** - * Set ONLY on `failover_flagged`: the abandoned flagged chat that was NOT sent - * into. Null otherwise. - */ - previous_chat_id?: string | null; -} - -export namespace MessageCreateResponse { - /** - * Why this line/chat was chosen. - */ - export interface FromSelection { - /** - * - `reused_active_chat` — reused an existing chat on its healthy line - * - `new_best_number` — created a new chat on the best available line - * - `failover_flagged` — no existing chat for these recipients was on a line that - * could send; created a new chat on a fresh line - */ - reason: 'reused_active_chat' | 'new_best_number' | 'failover_flagged'; - - /** - * True only when an existing chat was reused. - */ - reused_existing_chat: boolean; - } -} - -export interface MessageAddReactionResponse { - message?: string; - - status?: string; - - trace_id?: string; -} - -/** - * Response for sending a message to a chat - */ -export interface MessageUpdateAppCardResponse { - /** - * Unique identifier of the chat this message was sent to - */ - chat_id: string; - - /** - * A message that was sent (used in CreateChat and SendMessage responses) - */ - message: ChatsMessagesAPI.SentMessage; -} - -export interface MessageCreateParams { - /** - * Body param: Message content container. Groups all message-related fields - * together, separating the "what" (message content) from the "where" (routing - * fields like from/to). - * - * A message carries EITHER `parts` — text and attachments, which compose into one - * bubble — or a single `action`, which invokes an experience inside Linq's - * iMessage app. Never both: an app card is the whole message (Apple's `MSMessage` - * cannot coexist with text), so copy and a card are two sends, not one. - */ - message: ChatsAPI.MessageContent; - - /** - * Body param: Recipient handles (E.164 phone numbers or email addresses). One - * handle is a direct chat; multiple handles a group chat. Order-independent — the - * set identifies the chat. - */ - to: Array; - - /** - * Body param: Text-only fallback that **replaces** `message` ONLY on the failover - * branch — when a chat with these recipients already existed but its line was - * flagged, so a new chat is created on a fresh line. On that branch this text is - * sent as the single message instead of `message` (the recipient is on a new - * number, so you typically want a fresh-number-appropriate opener rather than the - * original content). Ignored otherwise (a healthy reuse, or genuine first - * contact). Carries no parts, media, or effects — exactly one message is ever - * sent. - */ - continuation_message?: MessageCreateParams.ContinuationMessage; - - /** - * Body param: Lines (E.164) not to pick for this send. Applies for this request - * only — nothing is remembered between calls. - * - * **Exclusion only affects picking a line for a new chat.** If `to` already has a - * chat, that chat is reused on its own line, and a chat on a non-excluded line is - * preferred when there is more than one. If the only chat these recipients have is - * on an excluded line, it is still reused — an exclusion never abandons a live - * chat or moves it to a new number. Check `from` in the response to see the line - * that was actually used. - * - * Numbers that are not your lines are ignored. Every entry must be E.164 — a value - * like `4155551234` is rejected rather than silently skipped. Excluding every one - * of your available lines returns 400 when a line has to be picked. - */ - exclude_from?: Array; - - /** - * Header param: Optional idempotency key for the send. Reuse the same key to - * safely retry without sending twice. May also be supplied as - * `message.idempotency_key`. - */ - 'Idempotency-Key'?: string; -} - -export namespace MessageCreateParams { - /** - * Text-only fallback that **replaces** `message` ONLY on the failover branch — - * when a chat with these recipients already existed but its line was flagged, so a - * new chat is created on a fresh line. On that branch this text is sent as the - * single message instead of `message` (the recipient is on a new number, so you - * typically want a fresh-number-appropriate opener rather than the original - * content). Ignored otherwise (a healthy reuse, or genuine first contact). Carries - * no parts, media, or effects — exactly one message is ever sent. - */ - export interface ContinuationMessage { - /** - * The replacement message text, sent as the single message on failover. - */ - text: string; - } -} - -export interface MessageUpdateParams { - /** - * New text content for the message part - */ - text: string; - - /** - * Index of the message part to edit. Defaults to 0. - */ - part_index?: number; -} - -export interface MessageAddReactionParams { - /** - * Whether to add or remove the reaction - */ - operation: 'add' | 'remove'; - - /** - * Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, - * emphasize, question. Custom emoji reactions have type "custom" with the actual - * emoji in the custom_emoji field. Sticker reactions have type "sticker" with - * sticker attachment details in the sticker field. - */ - type: Shared.ReactionType; - - /** - * Custom emoji string. Required when type is "custom". - */ - custom_emoji?: string; - - /** - * Optional index of the message part to react to. If not provided, reacts to the - * entire message (part 0). - */ - part_index?: number; -} - -export interface MessageListMessagesThreadParams extends ListMessagesPaginationParams { - /** - * Sort order for messages (asc = oldest first, desc = newest first) - */ - order?: 'asc' | 'desc'; -} - -export interface MessageUpdateAppCardParams { - /** - * Visible layout of the card. At least one of `caption`, `subcaption`, - * `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise - * the card renders as an empty bubble. - * - * `image_url` displays a preview image at the top of the card. The image renders - * on the recipient's card whether or not they have your app installed. The small - * icon beside the caption is the app's own icon and is not settable here. - * - * `* Note - requires a trusted chat w/ inbound activity` - * - * `image_title` and `image_subtitle` render as text overlaid on the image (title - * bold, subtitle beneath it). They only appear when `image_url` is set — without - * an image there is nothing to overlay — so setting either without `image_url` is - * rejected. - */ - layout: MessageUpdateAppCardParams.Layout; - - /** - * Text shown on surfaces that cannot render the card (notifications, lock screen). - * Defaults to the caption when omitted. - */ - fallback_text?: string; - - /** - * Whether the updated card renders as your app's interactive balloon for - * recipients who have your iMessage app installed. `true` (default) lets your - * installed extension draw its live view; `false` always shows the static `layout` - * card. Recipients without your app always see the static card regardless of this - * flag. - * - * Defaults to `true` when omitted — it is **not** inherited from the original - * card. To keep a card static across updates, re-send `interactive: false` on each - * update. - */ - interactive?: boolean; - - /** - * URL the recipient's app opens when they tap the updated card. - */ - url?: string; -} - -export namespace MessageUpdateAppCardParams { - /** - * Visible layout of the card. At least one of `caption`, `subcaption`, - * `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise - * the card renders as an empty bubble. - * - * `image_url` displays a preview image at the top of the card. The image renders - * on the recipient's card whether or not they have your app installed. The small - * icon beside the caption is the app's own icon and is not settable here. - * - * `* Note - requires a trusted chat w/ inbound activity` - * - * `image_title` and `image_subtitle` render as text overlaid on the image (title - * bold, subtitle beneath it). They only appear when `image_url` is set — without - * an image there is nothing to overlay — so setting either without `image_url` is - * rejected. - */ - export interface Layout { - /** - * Primary label, top-left and bold. - */ - caption?: string; - - /** - * Text shown below `image_title`, overlaid on the card image. Requires - * `image_url`. - */ - image_subtitle?: string; - - /** - * Bold text overlaid on the card image. Requires `image_url` (rejected without - * it). - */ - image_title?: string; - - /** - * URL of an image (JPEG, PNG, HEIF, or WebP) to display as the card's preview - * image; an unreachable or non-image URL returns a validation error. Renders for - * all recipients regardless of whether they have the app. Note - requires a - * trusted chat w/ inbound activity. In responses, this is the re-hosted - * `cdn.linqapp.com` copy of the image you supplied, not your original URL. - */ - image_url?: string; - - /** - * Secondary label, below `caption` on the left. - */ - subcaption?: string; - - /** - * Label shown top-right. - */ - trailing_caption?: string; - - /** - * Label shown below `trailing_caption`, on the right. - */ - trailing_subcaption?: string; - } -} - -export declare namespace Messages { - export { - type Message as Message, - type MessageEffect as MessageEffect, - type ReplyTo as ReplyTo, - type MessageCreateResponse as MessageCreateResponse, - type MessageAddReactionResponse as MessageAddReactionResponse, - type MessageUpdateAppCardResponse as MessageUpdateAppCardResponse, - type MessagesListMessagesPagination as MessagesListMessagesPagination, - type MessageCreateParams as MessageCreateParams, - type MessageUpdateParams as MessageUpdateParams, - type MessageAddReactionParams as MessageAddReactionParams, - type MessageListMessagesThreadParams as MessageListMessagesThreadParams, - type MessageUpdateAppCardParams as MessageUpdateAppCardParams, - }; -} +export * from './messages/index'; diff --git a/src/resources/messages/index.ts b/src/resources/messages/index.ts new file mode 100644 index 0000000..2de2ee1 --- /dev/null +++ b/src/resources/messages/index.ts @@ -0,0 +1,18 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export { + Messages, + type Message, + type MessageEffect, + type ReplyTo, + type MessageCreateResponse, + type MessageAddReactionResponse, + type MessageUpdateAppCardResponse, + type MessageCreateParams, + type MessageUpdateParams, + type MessageAddReactionParams, + type MessageListMessagesThreadParams, + type MessageUpdateAppCardParams, + type MessagesListMessagesPagination, +} from './messages'; +export { Poll, type PollAddOptionsParams, type PollVoteParams } from './poll'; diff --git a/src/resources/messages/messages.ts b/src/resources/messages/messages.ts new file mode 100644 index 0000000..08563ce --- /dev/null +++ b/src/resources/messages/messages.ts @@ -0,0 +1,958 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../core/resource'; +import * as Shared from '../shared'; +import * as ChatsAPI from '../chats/chats'; +import * as MessagesAPI from '../chats/messages'; +import * as PollAPI from './poll'; +import { Poll, PollAddOptionsParams, PollVoteParams } from './poll'; +import { APIPromise } from '../../core/api-promise'; +import { + ListMessagesPagination, + type ListMessagesPaginationParams, + PagePromise, +} from '../../core/pagination'; +import { buildHeaders } from '../../internal/headers'; +import { RequestOptions } from '../../internal/request-options'; +import { path } from '../../internal/utils/path'; + +/** + * Messages are individual communications within a chat thread. + * + * Messages can include text, media attachments, rich link previews, special effects + * (like confetti or fireworks), and reactions. All messages are associated with a + * specific chat and sent from a phone number you own. + * + * Messages support delivery status tracking, read receipts, and editing capabilities. + * + * ## Rich Link Previews + * + * Send a URL as a `link` part to deliver it with a rich preview card showing the + * page's title, description, and image (when available). A `link` part must be the + * **only** part in the message — it cannot be combined with text or media parts. + * To send a URL without a preview card, include it in a `text` part instead. + * + * **Limitations:** + * - A `link` part cannot be combined with other parts in the same message. + * - Maximum URL length: 2,048 characters. + * + * ## Ephemeral Messages (Privacy Tier) + * + * For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + * + * You can request it at two scopes: + * + * | Scope | Effect | + * |---|---| + * | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + * | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + * + * **Behavioral differences vs the standard default:** + * + * | Aspect | Standard | Ephemeral | + * |---|---|---| + * | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + * | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + * | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + * | Cross-partner isolation | Enforced | Enforced | + * + * **How the 24-hour window works:** + * + * - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + * - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + * - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + * + * **What you observe:** + * + * - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + * - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + * - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + * + * **When to choose ephemeral:** + * + * - You have a compliance requirement that the platform must not retain message content beyond a short window. + * - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + * - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + * + * **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. + */ +export class Messages extends APIResource { + poll: PollAPI.Poll = new PollAPI.Poll(this._client); + + /** + * Send a message to one or more recipients **without supplying a `from` number**. + * Linq resolves both the sending line and the target chat for you, then returns + * exactly which line was used, which chat the message landed in, whether a new + * chat was created, and every resulting message id. + * + * This fuses "create chat" and "send message" behind a single message-centric + * resource. Provide only the recipients (`to`) and the `message`; the platform + * decides the rest. + * + * ## How the from-number and chat are chosen + * + * - **Reuse** — if a chat with exactly these recipients already exists on a line + * that can still send, the message is sent into that chat on its existing line + * (`from_selection.reason = reused_active_chat`). The most-recently-active such + * chat wins; chats stranded on flagged lines (e.g. by an earlier failover) are + * skipped. + * - **New** — if no such chat exists, a new chat is created on the best available + * line (`from_selection.reason = new_best_number`). + * - **Failover** — if matching chats exist but none is on a line that can send, a + * **new** chat is created on a fresh best line and the flagged chat is abandoned + * (`from_selection.reason = failover_flagged`, `previous_chat_id` set). If you + * supply `continuation_message`, that text is sent as the single message INSTEAD + * of `message` (useful as a fresh-number-appropriate opener). Exactly one + * message is sent either way. + * + * Recipients (`to`) are an order-independent set: a single handle is a direct + * chat, multiple handles a group chat. + * + * ## Excluding lines + * + * `exclude_from` keeps specific lines out of **this** send's line pick. It only + * affects picking a line for a new chat — an existing chat is always reused on its + * own line, preferring a chat on a non-excluded line when the recipients have more + * than one. An exclusion never abandons a live chat or moves it to a new number, + * so if the only chat these recipients have is on an excluded line, that chat is + * still used. `from` tells you the line that was actually used. + * + * ## Differences from POST /v3/chats + * + * - The first message **may contain a link** (including for a newly created chat). + * Note: sending a link as the very first message on a freshly selected line can + * elevate that line's flagging risk — it is allowed, not recommended. + * - Voice memos are **not** supported here. To send an iMessage voice-memo bubble, + * use `POST /v3/chats/{chatId}/voicememo` with a known chat id. + * + * ## Service preference, effects, decorations + * + * Set `message.preferred_service` (`iMessage` | `RCS` | `SMS`), `message.effect`, + * and per-part `text_decorations` exactly as on the other send endpoints. + * + * Always responds `202 Accepted` — chat creation is incidental to the send. + * + * @example + * ```ts + * const message = await client.messages.create({ + * message: { + * parts: [ + * { + * type: 'text', + * value: + * 'Hi! Thanks for reaching out — how can we help?', + * }, + * ], + * }, + * to: ['+14155559876'], + * }); + * ``` + */ + create(params: MessageCreateParams, options?: RequestOptions): APIPromise { + const { 'Idempotency-Key': idempotencyKey, ...body } = params; + return this._client.post('/v3/messages', { + body, + ...options, + headers: buildHeaders([ + { ...(idempotencyKey != null ? { 'Idempotency-Key': idempotencyKey } : undefined) }, + options?.headers, + ]), + }); + } + + /** + * Retrieve a specific message by its ID. This endpoint returns the full message + * details including text, attachments, reactions, and metadata. + * + * @example + * ```ts + * const message = await client.messages.retrieve( + * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', + * ); + * ``` + */ + retrieve(messageID: string, options?: RequestOptions): APIPromise { + return this._client.get(path`/v3/messages/${messageID}`, options); + } + + /** + * Edit the text content of a specific part of a previously sent message. + * + * **Note:** A message can be edited up to 5 times, and only within 15 minutes of + * when it was originally sent. + * + * @example + * ```ts + * const message = await client.messages.update( + * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', + * { text: 'This is the edited message content' }, + * ); + * ``` + */ + update(messageID: string, body: MessageUpdateParams, options?: RequestOptions): APIPromise { + return this._client.patch(path`/v3/messages/${messageID}`, { body, ...options }); + } + + /** + * Deletes a message from the Linq API only. This does NOT unsend or remove the + * message from the actual chat — recipients will still see the message. Re-sending + * with a deleted message's idempotency key returns 404 — a deleted message is + * never resent. + * + * @example + * ```ts + * await client.messages.delete( + * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', + * ); + * ``` + */ + delete(messageID: string, options?: RequestOptions): APIPromise { + return this._client.delete(path`/v3/messages/${messageID}`, { + ...options, + headers: buildHeaders([{ Accept: '*/*' }, options?.headers]), + }); + } + + /** + * Add or remove emoji reactions to messages. Reactions let users express their + * response to a message without sending a new message. + * + * **Supported Reactions:** + * + * - love ❤️ + * - like 👍 + * - dislike 👎 + * - laugh 😂 + * - emphasize ‼️ + * - question ❓ + * - custom - any emoji (use `custom_emoji` field to specify) + * + * @example + * ```ts + * const response = await client.messages.addReaction( + * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', + * { operation: 'add', type: 'love' }, + * ); + * ``` + */ + addReaction( + messageID: string, + body: MessageAddReactionParams, + options?: RequestOptions, + ): APIPromise { + return this._client.post(path`/v3/messages/${messageID}/reactions`, { body, ...options }); + } + + /** + * Retrieve all messages in a conversation thread. Given any message ID in the + * thread, returns the originator message and all replies in chronological order. + * + * If the message is not part of a thread, returns just that single message. + * + * Supports pagination and configurable ordering. + * + * @example + * ```ts + * // Automatically fetches more pages as needed. + * for await (const message of client.messages.listMessagesThread( + * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', + * )) { + * // ... + * } + * ``` + */ + listMessagesThread( + messageID: string, + query: MessageListMessagesThreadParams | null | undefined = {}, + options?: RequestOptions, + ): PagePromise { + return this._client.getAPIList(path`/v3/messages/${messageID}/thread`, ListMessagesPagination, { + query, + ...options, + }); + } + + /** + * Replaces a previously delivered `imessage_app` card on the recipient's screen + * with new content, instead of posting a new bubble (like a game move redrawing + * the board). + * + * The update is delivered as a **new message** with its own id and delivery + * lifecycle (`message.sent` / `message.delivered` / `message.failed` webhooks fire + * for the new id). To update the card again, reference the message id returned by + * this call. + * + * Constraints: + * + * - The referenced message must be an `imessage_app` card sent by you (`400` + * otherwise — inbound cards cannot be updated). + * - The referenced card must already be delivered (`409` otherwise — retry after + * the `message.delivered` webhook for it). + * - The app identity (`team_id`, `bundle_id`, name) is inherited from the original + * card and cannot change; only `url`, `fallback_text`, and `layout` are + * replaced. + * - iMessage-only, like all app cards. + * - Concurrent updates against the same card are not serialized server-side; the + * last one delivered wins on the recipient's screen. Serialize updates by always + * referencing the message id returned by the previous call. + * + * @example + * ```ts + * const response = await client.messages.updateAppCard( + * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', + * { + * layout: { caption: 'Score: 2 – 1' }, + * fallback_text: 'Score update', + * url: 'https://app.example.com/card?game=7f3a&move=2', + * }, + * ); + * ``` + */ + updateAppCard( + messageID: string, + body: MessageUpdateAppCardParams, + options?: RequestOptions, + ): APIPromise { + return this._client.post(path`/v3/messages/${messageID}/update`, { body, ...options }); + } +} + +export type MessagesListMessagesPagination = ListMessagesPagination; + +export interface Message { + /** + * Unique identifier for the message + */ + id: string; + + /** + * ID of the chat this message belongs to + */ + chat_id: string; + + /** + * When the message was created + */ + created_at: string; + + /** + * Current delivery status of a message + */ + delivery_status: 'pending' | 'queued' | 'sent' | 'delivered' | 'received' | 'read' | 'failed'; + + /** + * @deprecated DEPRECATED: Use `delivery_status` instead (true when + * `delivery_status` is `delivered` or `read`). Whether the message has been + * delivered. + */ + is_delivered: boolean; + + /** + * Whether this message was sent by the authenticated user + */ + is_from_me: boolean; + + /** + * @deprecated DEPRECATED: Use `delivery_status == "read"` instead. Whether the + * message has been read. + */ + is_read: boolean; + + /** + * When the message was last updated + */ + updated_at: string; + + /** + * When the message was delivered + */ + delivered_at?: string | null; + + /** + * iMessage effect applied to a message (screen or bubble effect) + */ + effect?: MessageEffect | null; + + /** + * @deprecated DEPRECATED: Use from_handle instead. Phone number of the message + * sender. + */ + from?: string | null; + + /** + * The sender of this message as a full handle object + */ + from_handle?: Shared.ChatHandle | null; + + /** + * Message parts in order (text, media, and link) + */ + parts?: Array< + | Shared.TextPartResponse + | Shared.MediaPartResponse + | Shared.LinkPartResponse + | Message.IMessageAppPartResponse + > | null; + + /** + * Messaging service type + */ + preferred_service?: Shared.ServiceType | null; + + /** + * When the message was read + */ + read_at?: string | null; + + /** + * Present only when this message was recovered by reconciliation rather than + * delivered live, and set to the time of that recovery. The field is omitted + * entirely for normally-delivered messages, which is the overwhelming majority. + * When present, expect `sent_at` to be substantially earlier — the message is + * genuine but was ingested late, so it may not have appeared in earlier reads of + * this conversation. + */ + reconciled_at?: string; + + /** + * Indicates this message is a threaded reply to another message + */ + reply_to?: ReplyTo | null; + + /** + * When the message was sent + */ + sent_at?: string | null; + + /** + * Messaging service type + */ + service?: Shared.ServiceType | null; +} + +export namespace Message { + /** + * An iMessage app card part. + */ + export interface IMessageAppPartResponse { + /** + * Identifies the iMessage app (Messages app extension) that backs the card. + */ + app: IMessageAppPartResponse.App; + + /** + * Visible layout of the card. At least one of `caption`, `subcaption`, + * `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise + * the card renders as an empty bubble. + * + * `image_url` displays a preview image at the top of the card. The image renders + * on the recipient's card whether or not they have your app installed. The small + * icon beside the caption is the app's own icon and is not settable here. + * + * `* Note - requires a trusted chat w/ inbound activity` + * + * `image_title` and `image_subtitle` render as text overlaid on the image (title + * bold, subtitle beneath it). They only appear when `image_url` is set — without + * an image there is nothing to overlay — so setting either without `image_url` is + * rejected. + */ + layout: IMessageAppPartResponse.Layout; + + /** + * Reactions on this message part + */ + reactions: Array | null; + + /** + * Indicates this is an iMessage app card part. + */ + type: 'imessage_app'; + + /** + * The URL delivered to the iMessage app on tap. + */ + url: string; + + /** + * Fallback text for surfaces that cannot render the card. + */ + fallback_text?: string | null; + } + + export namespace IMessageAppPartResponse { + /** + * Identifies the iMessage app (Messages app extension) that backs the card. + */ + export interface App { + /** + * Bundle identifier of the Messages app extension. Must not contain `:`. + */ + bundle_id: string; + + /** + * Display name of the app, shown by Messages' fallback UI. + */ + name: string; + + /** + * The app's 10-character uppercase alphanumeric team identifier. + */ + team_id: string; + + /** + * The owning app's App Store id (optional). When set, recipients without the + * iMessage app installed see a "Get the app" affordance. + */ + app_store_id?: number; + } + + /** + * Visible layout of the card. At least one of `caption`, `subcaption`, + * `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise + * the card renders as an empty bubble. + * + * `image_url` displays a preview image at the top of the card. The image renders + * on the recipient's card whether or not they have your app installed. The small + * icon beside the caption is the app's own icon and is not settable here. + * + * `* Note - requires a trusted chat w/ inbound activity` + * + * `image_title` and `image_subtitle` render as text overlaid on the image (title + * bold, subtitle beneath it). They only appear when `image_url` is set — without + * an image there is nothing to overlay — so setting either without `image_url` is + * rejected. + */ + export interface Layout { + /** + * Primary label, top-left and bold. + */ + caption?: string; + + /** + * Text shown below `image_title`, overlaid on the card image. Requires + * `image_url`. + */ + image_subtitle?: string; + + /** + * Bold text overlaid on the card image. Requires `image_url` (rejected without + * it). + */ + image_title?: string; + + /** + * URL of an image (JPEG, PNG, HEIF, or WebP) to display as the card's preview + * image; an unreachable or non-image URL returns a validation error. Renders for + * all recipients regardless of whether they have the app. Note - requires a + * trusted chat w/ inbound activity. In responses, this is the re-hosted + * `cdn.linqapp.com` copy of the image you supplied, not your original URL. + */ + image_url?: string; + + /** + * Secondary label, below `caption` on the left. + */ + subcaption?: string; + + /** + * Label shown top-right. + */ + trailing_caption?: string; + + /** + * Label shown below `trailing_caption`, on the right. + */ + trailing_subcaption?: string; + } + } +} + +/** + * iMessage effect applied to a message (screen or bubble effect) + */ +export interface MessageEffect { + /** + * Name of the effect. Common values: + * + * - Screen effects: confetti, fireworks, lasers, sparkles, celebration, hearts, + * love, balloons, happy_birthday, echo, spotlight + * - Bubble effects: slam, loud, gentle, invisible + */ + name?: string; + + /** + * Type of effect + */ + type?: 'screen' | 'bubble'; +} + +/** + * Indicates this message is a threaded reply to another message + */ +export interface ReplyTo { + /** + * The ID of the message to reply to + */ + message_id: string; + + /** + * The specific message part to reply to (0-based index). Defaults to 0 (first + * part) if not provided. Use this when replying to a specific part of a multipart + * message. + */ + part_index?: number; +} + +/** + * Result of an auto-from send. Self-describing: which line was used, which chat + * the message landed in, whether a new chat was created, and the resulting message + * id(s). + */ +export interface MessageCreateResponse { + /** + * The resolved chat (reused or newly created) the message landed in. + */ + chat_id: string; + + /** + * True when a new chat was created (new or failover), false on reuse. + */ + created_new_chat: boolean; + + /** + * The line (E.164) the message was actually sent from. + */ + from: string; + + /** + * Why this line/chat was chosen. + */ + from_selection: MessageCreateResponse.FromSelection; + + /** + * Participants of the resolved chat. + */ + handles: Array; + + /** + * Whether the resolved chat is a group chat. + */ + is_group: boolean; + + /** + * A message that was sent (used in CreateChat and SendMessage responses) + */ + message: MessagesAPI.SentMessage; + + /** + * Messaging service type + */ + service: Shared.ServiceType; + + /** + * Set ONLY on `failover_flagged`: the abandoned flagged chat that was NOT sent + * into. Null otherwise. + */ + previous_chat_id?: string | null; +} + +export namespace MessageCreateResponse { + /** + * Why this line/chat was chosen. + */ + export interface FromSelection { + /** + * - `reused_active_chat` — reused an existing chat on its healthy line + * - `new_best_number` — created a new chat on the best available line + * - `failover_flagged` — no existing chat for these recipients was on a line that + * could send; created a new chat on a fresh line + */ + reason: 'reused_active_chat' | 'new_best_number' | 'failover_flagged'; + + /** + * True only when an existing chat was reused. + */ + reused_existing_chat: boolean; + } +} + +export interface MessageAddReactionResponse { + message?: string; + + status?: string; + + trace_id?: string; +} + +/** + * Response for sending a message to a chat + */ +export interface MessageUpdateAppCardResponse { + /** + * Unique identifier of the chat this message was sent to + */ + chat_id: string; + + /** + * A message that was sent (used in CreateChat and SendMessage responses) + */ + message: MessagesAPI.SentMessage; +} + +export interface MessageCreateParams { + /** + * Body param: Message content container. Groups all message-related fields + * together, separating the "what" (message content) from the "where" (routing + * fields like from/to). + * + * A message carries EITHER `parts` — text and attachments, which compose into one + * bubble — or a single `action`, which invokes an experience inside Linq's + * iMessage app. Never both: an app card is the whole message (Apple's `MSMessage` + * cannot coexist with text), so copy and a card are two sends, not one. + */ + message: ChatsAPI.MessageContent; + + /** + * Body param: Recipient handles (E.164 phone numbers or email addresses). One + * handle is a direct chat; multiple handles a group chat. Order-independent — the + * set identifies the chat. + */ + to: Array; + + /** + * Body param: Text-only fallback that **replaces** `message` ONLY on the failover + * branch — when a chat with these recipients already existed but its line was + * flagged, so a new chat is created on a fresh line. On that branch this text is + * sent as the single message instead of `message` (the recipient is on a new + * number, so you typically want a fresh-number-appropriate opener rather than the + * original content). Ignored otherwise (a healthy reuse, or genuine first + * contact). Carries no parts, media, or effects — exactly one message is ever + * sent. + */ + continuation_message?: MessageCreateParams.ContinuationMessage; + + /** + * Body param: Lines (E.164) not to pick for this send. Applies for this request + * only — nothing is remembered between calls. + * + * **Exclusion only affects picking a line for a new chat.** If `to` already has a + * chat, that chat is reused on its own line, and a chat on a non-excluded line is + * preferred when there is more than one. If the only chat these recipients have is + * on an excluded line, it is still reused — an exclusion never abandons a live + * chat or moves it to a new number. Check `from` in the response to see the line + * that was actually used. + * + * Numbers that are not your lines are ignored. Every entry must be E.164 — a value + * like `4155551234` is rejected rather than silently skipped. Excluding every one + * of your available lines returns 400 when a line has to be picked. + */ + exclude_from?: Array; + + /** + * Body param: Send even though the recipient asked you to stop (`403`, error code + * `2024`). Applies to this request only: the opt-out stays in place, so the next + * send without this flag is rejected again. Every override is recorded against + * your API key. + */ + override_optout?: boolean; + + /** + * Header param: Optional idempotency key for the send. Reuse the same key to + * safely retry without sending twice. May also be supplied as + * `message.idempotency_key`. + */ + 'Idempotency-Key'?: string; +} + +export namespace MessageCreateParams { + /** + * Text-only fallback that **replaces** `message` ONLY on the failover branch — + * when a chat with these recipients already existed but its line was flagged, so a + * new chat is created on a fresh line. On that branch this text is sent as the + * single message instead of `message` (the recipient is on a new number, so you + * typically want a fresh-number-appropriate opener rather than the original + * content). Ignored otherwise (a healthy reuse, or genuine first contact). Carries + * no parts, media, or effects — exactly one message is ever sent. + */ + export interface ContinuationMessage { + /** + * The replacement message text, sent as the single message on failover. + */ + text: string; + } +} + +export interface MessageUpdateParams { + /** + * New text content for the message part + */ + text: string; + + /** + * Index of the message part to edit. Defaults to 0. + */ + part_index?: number; +} + +export interface MessageAddReactionParams { + /** + * Whether to add or remove the reaction + */ + operation: 'add' | 'remove'; + + /** + * Type of reaction. Standard iMessage tapbacks are love, like, dislike, laugh, + * emphasize, question. Custom emoji reactions have type "custom" with the actual + * emoji in the custom_emoji field. Sticker reactions have type "sticker" with + * sticker attachment details in the sticker field. + */ + type: Shared.ReactionType; + + /** + * Custom emoji string. Required when type is "custom". + */ + custom_emoji?: string; + + /** + * Optional index of the message part to react to. If not provided, reacts to the + * entire message (part 0). + */ + part_index?: number; +} + +export interface MessageListMessagesThreadParams extends ListMessagesPaginationParams { + /** + * Sort order for messages (asc = oldest first, desc = newest first) + */ + order?: 'asc' | 'desc'; +} + +export interface MessageUpdateAppCardParams { + /** + * Visible layout of the card. At least one of `caption`, `subcaption`, + * `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise + * the card renders as an empty bubble. + * + * `image_url` displays a preview image at the top of the card. The image renders + * on the recipient's card whether or not they have your app installed. The small + * icon beside the caption is the app's own icon and is not settable here. + * + * `* Note - requires a trusted chat w/ inbound activity` + * + * `image_title` and `image_subtitle` render as text overlaid on the image (title + * bold, subtitle beneath it). They only appear when `image_url` is set — without + * an image there is nothing to overlay — so setting either without `image_url` is + * rejected. + */ + layout: MessageUpdateAppCardParams.Layout; + + /** + * Text shown on surfaces that cannot render the card (notifications, lock screen). + * Defaults to the caption when omitted. + */ + fallback_text?: string; + + /** + * Whether the updated card renders as your app's interactive balloon for + * recipients who have your iMessage app installed. `true` (default) lets your + * installed extension draw its live view; `false` always shows the static `layout` + * card. Recipients without your app always see the static card regardless of this + * flag. + * + * Defaults to `true` when omitted — it is **not** inherited from the original + * card. To keep a card static across updates, re-send `interactive: false` on each + * update. + */ + interactive?: boolean; + + /** + * URL the recipient's app opens when they tap the updated card. + */ + url?: string; +} + +export namespace MessageUpdateAppCardParams { + /** + * Visible layout of the card. At least one of `caption`, `subcaption`, + * `trailing_caption`, `trailing_subcaption`, or `image_url` must be set, otherwise + * the card renders as an empty bubble. + * + * `image_url` displays a preview image at the top of the card. The image renders + * on the recipient's card whether or not they have your app installed. The small + * icon beside the caption is the app's own icon and is not settable here. + * + * `* Note - requires a trusted chat w/ inbound activity` + * + * `image_title` and `image_subtitle` render as text overlaid on the image (title + * bold, subtitle beneath it). They only appear when `image_url` is set — without + * an image there is nothing to overlay — so setting either without `image_url` is + * rejected. + */ + export interface Layout { + /** + * Primary label, top-left and bold. + */ + caption?: string; + + /** + * Text shown below `image_title`, overlaid on the card image. Requires + * `image_url`. + */ + image_subtitle?: string; + + /** + * Bold text overlaid on the card image. Requires `image_url` (rejected without + * it). + */ + image_title?: string; + + /** + * URL of an image (JPEG, PNG, HEIF, or WebP) to display as the card's preview + * image; an unreachable or non-image URL returns a validation error. Renders for + * all recipients regardless of whether they have the app. Note - requires a + * trusted chat w/ inbound activity. In responses, this is the re-hosted + * `cdn.linqapp.com` copy of the image you supplied, not your original URL. + */ + image_url?: string; + + /** + * Secondary label, below `caption` on the left. + */ + subcaption?: string; + + /** + * Label shown top-right. + */ + trailing_caption?: string; + + /** + * Label shown below `trailing_caption`, on the right. + */ + trailing_subcaption?: string; + } +} + +Messages.Poll = Poll; + +export declare namespace Messages { + export { + type Message as Message, + type MessageEffect as MessageEffect, + type ReplyTo as ReplyTo, + type MessageCreateResponse as MessageCreateResponse, + type MessageAddReactionResponse as MessageAddReactionResponse, + type MessageUpdateAppCardResponse as MessageUpdateAppCardResponse, + type MessagesListMessagesPagination as MessagesListMessagesPagination, + type MessageCreateParams as MessageCreateParams, + type MessageUpdateParams as MessageUpdateParams, + type MessageAddReactionParams as MessageAddReactionParams, + type MessageListMessagesThreadParams as MessageListMessagesThreadParams, + type MessageUpdateAppCardParams as MessageUpdateAppCardParams, + }; + + export { + Poll as Poll, + type PollAddOptionsParams as PollAddOptionsParams, + type PollVoteParams as PollVoteParams, + }; +} diff --git a/src/resources/messages/poll.ts b/src/resources/messages/poll.ts new file mode 100644 index 0000000..e40d5ba --- /dev/null +++ b/src/resources/messages/poll.ts @@ -0,0 +1,151 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../core/resource'; +import * as PollsAPI from '../chats/polls'; +import { APIPromise } from '../../core/api-promise'; +import { RequestOptions } from '../../internal/request-options'; +import { path } from '../../internal/utils/path'; + +/** + * Messages are individual communications within a chat thread. + * + * Messages can include text, media attachments, rich link previews, special effects + * (like confetti or fireworks), and reactions. All messages are associated with a + * specific chat and sent from a phone number you own. + * + * Messages support delivery status tracking, read receipts, and editing capabilities. + * + * ## Rich Link Previews + * + * Send a URL as a `link` part to deliver it with a rich preview card showing the + * page's title, description, and image (when available). A `link` part must be the + * **only** part in the message — it cannot be combined with text or media parts. + * To send a URL without a preview card, include it in a `text` part instead. + * + * **Limitations:** + * - A `link` part cannot be combined with other parts in the same message. + * - Maximum URL length: 2,048 characters. + * + * ## Ephemeral Messages (Privacy Tier) + * + * For regulated or sensitive conversations, opt in to the **ephemeral messages** tier by contacting your Linq support contact. When enabled, every message on the covered phone numbers is automatically given a fixed **24-hour retention window** — after that window the platform permanently deletes the message from Linq storage. There is no per-message flag; ephemerality is applied automatically based on your configuration. + * + * You can request it at two scopes: + * + * | Scope | Effect | + * |---|---| + * | **Partner-wide** | Every outbound and inbound message on every phone number under your account is retained for 24 hours, then deleted. | + * | **Per phone number** | Only the specified phone numbers have their messages auto-deleted. The rest follow the standard message-retention policy. | + * + * **Behavioral differences vs the standard default:** + * + * | Aspect | Standard | Ephemeral | + * |---|---|---| + * | Retention | Retained per the standard message-retention policy | **Hard backstop: 24 hours** from when the message is created | + * | After expiry | Message stays retrievable | Message is permanently deleted — `GET /v3/messages/{messageId}` returns `404` and it no longer appears in `GET /v3/chats/{chatId}/messages` | + * | Content on expiry | N/A | Text, formatting, and attachment references are scrubbed; the message is gone, not blanked out | + * | Cross-partner isolation | Enforced | Enforced | + * + * **How the 24-hour window works:** + * + * - The window is fixed at **24 hours from message creation** (`created_at`) and cannot be configured per message. + * - It mirrors the ephemeral *attachments* 1-day backstop, so a message and any media it carries expire together. + * - Expiry is delivery-independent — the clock starts when the message is created, not when it is delivered or read. + * + * **What you observe:** + * + * - **No expiry timestamp is exposed.** API responses and webhook payloads do not include the deletion time. If you need it, compute `created_at + 24h` yourself. + * - **No deletion webhook is sent.** There is no `message.deleted` event — a message simply stops being retrievable once its window passes. + * - **Delivery is unaffected.** Ephemeral messages send, deliver, and fire the usual `message.sent` / `message.received` and status webhooks exactly like standard messages. Only retention changes. + * + * **When to choose ephemeral:** + * + * - You have a compliance requirement that the platform must not retain message content beyond a short window. + * - The conversation is high-sensitivity (PHI, financial, identity verification) and you do not want it sitting in storage long-term. + * - Your application is the system of record — you capture what you need from the delivery webhook in real time and do not rely on reading message history back from Linq later. + * + * **Important:** ephemeral applies in *both directions* — messages you send **and** messages received by the phone numbers in that scope. Because Linq can no longer return the message after 24 hours, persist anything you need to keep from the webhook payload at the time it is delivered. + */ +export class Poll extends APIResource { + /** + * Return a poll's current results — its options, each option's voters, and the + * distinct total number of voters — by the poll-definition message's ID. + * + * @example + * ```ts + * const pollEnvelope = await client.messages.poll.retrieve( + * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', + * ); + * ``` + */ + retrieve(messageID: string, options?: RequestOptions): APIPromise { + return this._client.get(path`/v3/messages/${messageID}/poll`, options); + } + + /** + * Add one or more options to an existing poll. Options are **add-only and + * immutable** — you can append options but never edit or remove them (Apple + * constraint). Returns the full poll. + * + * @example + * ```ts + * const pollEnvelope = await client.messages.poll.addOptions( + * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', + * { options: [{ text: 'Pizza' }] }, + * ); + * ``` + */ + addOptions( + messageID: string, + body: PollAddOptionsParams, + options?: RequestOptions, + ): APIPromise { + return this._client.post(path`/v3/messages/${messageID}/poll/options`, { body, ...options }); + } + + /** + * Add or remove your line's vote on **one** poll option (per-option toggle — + * iMessage polls are toggled one option at a time). Returns the poll reflecting + * the toggle. + * + * @example + * ```ts + * const pollEnvelope = await client.messages.poll.vote( + * '69a37c7d-af4f-4b5e-af42-e28e98ce873a', + * { + * operation: 'add', + * option_id: '97ce8c17-7ef6-4bbc-a89a-6b93d189712f', + * }, + * ); + * ``` + */ + vote(messageID: string, body: PollVoteParams, options?: RequestOptions): APIPromise { + return this._client.post(path`/v3/messages/${messageID}/poll/votes`, { body, ...options }); + } +} + +export interface PollAddOptionsParams { + options: Array; +} + +export namespace PollAddOptionsParams { + export interface Option { + text: string; + } +} + +export interface PollVoteParams { + /** + * Add or remove your line's vote on the option. + */ + operation: 'add' | 'remove'; + + /** + * The option to toggle a vote on. + */ + option_id: string; +} + +export declare namespace Poll { + export { type PollAddOptionsParams as PollAddOptionsParams, type PollVoteParams as PollVoteParams }; +} diff --git a/src/resources/webhook-events.ts b/src/resources/webhook-events.ts index d7ba730..aeff877 100644 --- a/src/resources/webhook-events.ts +++ b/src/resources/webhook-events.ts @@ -159,6 +159,15 @@ export type WebhookEventType = | 'message.edited' | 'reaction.added' | 'reaction.removed' + | 'poll.received' + | 'poll.failed' + | 'poll.sent' + | 'poll.delivered' + | 'poll.read' + | 'poll.updated' + | 'poll.vote.added' + | 'poll.vote.removed' + | 'poll.reaction.added' | 'participant.added' | 'participant.removed' | 'chat.created' diff --git a/src/resources/webhooks.ts b/src/resources/webhooks.ts index 5f52fca..ab62932 100644 --- a/src/resources/webhooks.ts +++ b/src/resources/webhooks.ts @@ -186,15 +186,19 @@ export namespace MessageEventV2 { * to react. `doc_url` deep-links to the relevant section. * * `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - * `CANCEL`, `END`, or `QUIT`, and you should send nothing further on this chat. - * The keyword must be the whole trimmed message, never part of a longer one: - * `STOP` counts, `please stop` does not. Most keywords must match exactly, - * including case. `OPT OUT` is the exception — it matches in any casing, with or - * without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. - * It clears if they later send `START`, `OPTIN`, or `UNSTOP`, or if they keep - * replying on the chat — sustained two-way conversation is treated as a sign the - * stop keyword was a false positive. Suppressing sends to opted-out recipients is - * your responsibility — Linq surfaces the status but does not block the send. + * `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never + * part of a longer one: `STOP` counts, `please stop` does not. Most keywords must + * match exactly, including case. `OPT OUT` is the exception — it matches in any + * casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and + * `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, + * or if they keep replying on the chat — sustained two-way conversation is treated + * as a sign the stop keyword was a false positive. + * + * Linq enforces this: while a recipient is opted out, every send to them is + * rejected with `403` (error code `2024`) before the message is queued, across + * every chat and every line on your account. Nothing is delivered, including a + * final courtesy message — to send one, set `override_optout: true` on that single + * request. */ status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; @@ -1198,15 +1202,19 @@ export namespace MessageEditedWebhookEvent { * to react. `doc_url` deep-links to the relevant section. * * `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - * `CANCEL`, `END`, or `QUIT`, and you should send nothing further on this chat. - * The keyword must be the whole trimmed message, never part of a longer one: - * `STOP` counts, `please stop` does not. Most keywords must match exactly, - * including case. `OPT OUT` is the exception — it matches in any casing, with or - * without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. - * It clears if they later send `START`, `OPTIN`, or `UNSTOP`, or if they keep - * replying on the chat — sustained two-way conversation is treated as a sign the - * stop keyword was a false positive. Suppressing sends to opted-out recipients is - * your responsibility — Linq surfaces the status but does not block the send. + * `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never + * part of a longer one: `STOP` counts, `please stop` does not. Most keywords must + * match exactly, including case. `OPT OUT` is the exception — it matches in any + * casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and + * `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, + * or if they keep replying on the chat — sustained two-way conversation is treated + * as a sign the stop keyword was a false positive. + * + * Linq enforces this: while a recipient is opted out, every send to them is + * rejected with `403` (error code `2024`) before the message is queued, across + * every chat and every line on your account. Nothing is delivered, including a + * final courtesy message — to send one, set `override_optout: true` on that single + * request. */ status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; @@ -1631,15 +1639,19 @@ export namespace ChatCreatedWebhookEvent { * to react. `doc_url` deep-links to the relevant section. * * `OPTED_OUT` is terminal — the recipient sent `STOP`, `UNSUBSCRIBE`, `OPTOUT`, - * `CANCEL`, `END`, or `QUIT`, and you should send nothing further on this chat. - * The keyword must be the whole trimmed message, never part of a longer one: - * `STOP` counts, `please stop` does not. Most keywords must match exactly, - * including case. `OPT OUT` is the exception — it matches in any casing, with or - * without the space or a hyphen, so `opt out`, `Opt-Out` and `optout` all count. - * It clears if they later send `START`, `OPTIN`, or `UNSTOP`, or if they keep - * replying on the chat — sustained two-way conversation is treated as a sign the - * stop keyword was a false positive. Suppressing sends to opted-out recipients is - * your responsibility — Linq surfaces the status but does not block the send. + * `CANCEL`, `END`, or `QUIT`. The keyword must be the whole trimmed message, never + * part of a longer one: `STOP` counts, `please stop` does not. Most keywords must + * match exactly, including case. `OPT OUT` is the exception — it matches in any + * casing, with or without the space or a hyphen, so `opt out`, `Opt-Out` and + * `optout` all count. It clears if they later send `START`, `OPTIN`, or `UNSTOP`, + * or if they keep replying on the chat — sustained two-way conversation is treated + * as a sign the stop keyword was a false positive. + * + * Linq enforces this: while a recipient is opted out, every send to them is + * rejected with `403` (error code `2024`) before the message is queued, across + * every chat and every line on your account. Nothing is delivered, including a + * final courtesy message — to send one, set `override_optout: true` on that single + * request. */ status: 'HEALTHY' | 'AT_RISK' | 'CRITICAL' | 'OPTED_OUT'; @@ -2133,6 +2145,15 @@ export interface PhoneNumberStatusUpdatedWebhookEvent { | 'message.edited' | 'reaction.added' | 'reaction.removed' + | 'poll.received' + | 'poll.failed' + | 'poll.sent' + | 'poll.delivered' + | 'poll.read' + | 'poll.updated' + | 'poll.vote.added' + | 'poll.vote.removed' + | 'poll.reaction.added' | 'participant.added' | 'participant.removed' | 'chat.created' diff --git a/tests/api-resources/chats/chats.test.ts b/tests/api-resources/chats/chats.test.ts index 636c775..838b140 100644 --- a/tests/api-resources/chats/chats.test.ts +++ b/tests/api-resources/chats/chats.test.ts @@ -40,6 +40,8 @@ describe('resource chats', () => { { type: 'text', value: 'Hello! How can I help you today?', + mention: '+14155551234', + mention_range: [4, 9], text_decorations: [ { range: [0, 5], @@ -58,6 +60,7 @@ describe('resource chats', () => { reply_to: { message_id: '550e8400-e29b-41d4-a716-446655440000', part_index: 0 }, }, to: ['+12052532136'], + override_optout: false, }); }); diff --git a/tests/api-resources/chats/messages.test.ts b/tests/api-resources/chats/messages.test.ts index 167c7eb..f0981fa 100644 --- a/tests/api-resources/chats/messages.test.ts +++ b/tests/api-resources/chats/messages.test.ts @@ -61,6 +61,8 @@ describe('resource messages', () => { { type: 'text', value: 'Hello, world!', + mention: '+14155551234', + mention_range: [4, 9], text_decorations: [ { range: [0, 5], @@ -78,6 +80,7 @@ describe('resource messages', () => { preferred_service: 'iMessage', reply_to: { message_id: '550e8400-e29b-41d4-a716-446655440000', part_index: 0 }, }, + override_optout: false, }); }); }); diff --git a/tests/api-resources/chats/polls.test.ts b/tests/api-resources/chats/polls.test.ts new file mode 100644 index 0000000..3726f54 --- /dev/null +++ b/tests/api-resources/chats/polls.test.ts @@ -0,0 +1,31 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import LinqAPIV3 from '@linqapp/sdk'; + +const client = new LinqAPIV3({ + apiKey: 'My API Key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource polls', () => { + // Mock server tests are disabled + test.skip('create: only required params', async () => { + const responsePromise = client.chats.polls.create('550e8400-e29b-41d4-a716-446655440000', { + poll: { options: [{ text: 'Tacos' }, { text: 'Sushi' }] }, + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + // Mock server tests are disabled + test.skip('create: required and optional params', async () => { + const response = await client.chats.polls.create('550e8400-e29b-41d4-a716-446655440000', { + poll: { options: [{ text: 'Tacos' }, { text: 'Sushi' }], idempotency_key: 'poll-abc123' }, + }); + }); +}); diff --git a/tests/api-resources/messages.test.ts b/tests/api-resources/messages/messages.test.ts similarity index 98% rename from tests/api-resources/messages.test.ts rename to tests/api-resources/messages/messages.test.ts index 56cd797..352ddf9 100644 --- a/tests/api-resources/messages.test.ts +++ b/tests/api-resources/messages/messages.test.ts @@ -38,6 +38,8 @@ describe('resource messages', () => { { type: 'text', value: 'Hi! Thanks for reaching out — how can we help?', + mention: '+14155551234', + mention_range: [4, 9], text_decorations: [ { range: [0, 5], @@ -58,6 +60,7 @@ describe('resource messages', () => { to: ['+14155559876'], continuation_message: { text: "Hi, it's Acme Support reaching you from a new number." }, exclude_from: ['+12052535597'], + override_optout: false, 'Idempotency-Key': 'send-abc123xyz', }); }); diff --git a/tests/api-resources/messages/poll.test.ts b/tests/api-resources/messages/poll.test.ts new file mode 100644 index 0000000..c8e17f0 --- /dev/null +++ b/tests/api-resources/messages/poll.test.ts @@ -0,0 +1,66 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import LinqAPIV3 from '@linqapp/sdk'; + +const client = new LinqAPIV3({ + apiKey: 'My API Key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource poll', () => { + // Mock server tests are disabled + test.skip('retrieve', async () => { + const responsePromise = client.messages.poll.retrieve('69a37c7d-af4f-4b5e-af42-e28e98ce873a'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + // Mock server tests are disabled + test.skip('addOptions: only required params', async () => { + const responsePromise = client.messages.poll.addOptions('69a37c7d-af4f-4b5e-af42-e28e98ce873a', { + options: [{ text: 'Pizza' }], + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + // Mock server tests are disabled + test.skip('addOptions: required and optional params', async () => { + const response = await client.messages.poll.addOptions('69a37c7d-af4f-4b5e-af42-e28e98ce873a', { + options: [{ text: 'Pizza' }], + }); + }); + + // Mock server tests are disabled + test.skip('vote: only required params', async () => { + const responsePromise = client.messages.poll.vote('69a37c7d-af4f-4b5e-af42-e28e98ce873a', { + operation: 'add', + option_id: '97ce8c17-7ef6-4bbc-a89a-6b93d189712f', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + // Mock server tests are disabled + test.skip('vote: required and optional params', async () => { + const response = await client.messages.poll.vote('69a37c7d-af4f-4b5e-af42-e28e98ce873a', { + operation: 'add', + option_id: '97ce8c17-7ef6-4bbc-a89a-6b93d189712f', + }); + }); +});