From b1ea65b3f68e3f92b8055619a32c65a09694d9d2 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 16:32:50 -0700 Subject: [PATCH 1/6] feat(linq): audit fixes + native auto-registering webhook trigger Tools/block audit (validated against the live Linq partner API + OpenAPI spec): - create_chat: read the sent message from top-level response.message (was always null) - get_message/edit_message: expose canonical deliveryStatus; mark is_delivered/is_read deprecated - send_message: fall back to from_handle.service/preferred_service for service output - list_phone_numbers: migrate deprecated health_status to reputation; add forwardingNumber; guard JSON parse - check_imessage/check_rcs: guard response.json() parse - mark_chat_read: note 1:1-only / group no-op behavior Native webhook trigger (auto register + deregister): - 6 triggers (message received/delivered/failed/read, reaction added, all-events) - Standard Webhooks signature verification (HMAC-SHA256, whsec_ secret) - createSubscription/deleteSubscription manage the Linq subscription lifecycle - event_id idempotency; full 27-value WebhookEventType enum for all-events - regenerated docs --- .../content/docs/en/integrations/linq.mdx | 161 ++++++++++- apps/sim/blocks/blocks/linq.ts | 19 ++ apps/sim/lib/integrations/integrations.json | 43 ++- apps/sim/lib/webhooks/providers/linq.ts | 266 ++++++++++++++++++ apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/tools/linq/check_imessage.ts | 6 +- apps/sim/tools/linq/check_rcs.ts | 6 +- apps/sim/tools/linq/create_chat.ts | 2 +- apps/sim/tools/linq/edit_message.ts | 15 +- apps/sim/tools/linq/get_message.ts | 15 +- apps/sim/tools/linq/list_phone_numbers.ts | 17 +- apps/sim/tools/linq/mark_chat_read.ts | 3 +- apps/sim/tools/linq/send_message.ts | 3 +- apps/sim/tools/linq/types.ts | 2 + apps/sim/triggers/linq/index.ts | 6 + apps/sim/triggers/linq/message_delivered.ts | 38 +++ apps/sim/triggers/linq/message_failed.ts | 38 +++ apps/sim/triggers/linq/message_read.ts | 38 +++ apps/sim/triggers/linq/message_received.ts | 39 +++ apps/sim/triggers/linq/reaction_added.ts | 38 +++ apps/sim/triggers/linq/utils.ts | 143 ++++++++++ apps/sim/triggers/linq/webhook.ts | 40 +++ apps/sim/triggers/registry.ts | 14 + 23 files changed, 925 insertions(+), 29 deletions(-) create mode 100644 apps/sim/lib/webhooks/providers/linq.ts create mode 100644 apps/sim/triggers/linq/index.ts create mode 100644 apps/sim/triggers/linq/message_delivered.ts create mode 100644 apps/sim/triggers/linq/message_failed.ts create mode 100644 apps/sim/triggers/linq/message_read.ts create mode 100644 apps/sim/triggers/linq/message_received.ts create mode 100644 apps/sim/triggers/linq/reaction_added.ts create mode 100644 apps/sim/triggers/linq/utils.ts create mode 100644 apps/sim/triggers/linq/webhook.ts diff --git a/apps/docs/content/docs/en/integrations/linq.mdx b/apps/docs/content/docs/en/integrations/linq.mdx index 82ad460cfe1..7f4c035589b 100644 --- a/apps/docs/content/docs/en/integrations/linq.mdx +++ b/apps/docs/content/docs/en/integrations/linq.mdx @@ -277,8 +277,9 @@ Edit the text of a sent message (up to 5 times, within 15 minutes of sending; iM | `id` | string | Message ID | | `chatId` | string | ID of the chat the message belongs to | | `isFromMe` | boolean | Whether the message was sent by you | -| `isDelivered` | boolean | Whether the message was delivered | -| `isRead` | boolean | Whether the message was read | +| `deliveryStatus` | string | Delivery status \(pending, queued, sent, delivered, received, read, failed\) | +| `isDelivered` | boolean | Whether the message was delivered \(deprecated; use deliveryStatus\) | +| `isRead` | boolean | Whether the message was read \(deprecated; use deliveryStatus\) | | `service` | string | Delivery service \(iMessage, SMS, RCS\) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | @@ -374,8 +375,9 @@ Retrieve a single message by ID, including parts, reactions, and delivery status | `id` | string | Message ID | | `chatId` | string | ID of the chat the message belongs to | | `isFromMe` | boolean | Whether the message was sent by you | -| `isDelivered` | boolean | Whether the message was delivered | -| `isRead` | boolean | Whether the message was read | +| `deliveryStatus` | string | Delivery status \(pending, queued, sent, delivered, received, read, failed\) | +| `isDelivered` | boolean | Whether the message was delivered \(deprecated; use deliveryStatus\) | +| `isRead` | boolean | Whether the message was read \(deprecated; use deliveryStatus\) | | `service` | string | Delivery service \(iMessage, SMS, RCS\) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 update timestamp | @@ -483,7 +485,8 @@ List all phone numbers assigned to your partner account, with line health | `phoneNumbers` | array | Phone numbers assigned to the account | | ↳ `id` | string | Phone number ID | | ↳ `phoneNumber` | string | Phone number in E.164 format | -| ↳ `healthStatus` | json | Line health status \(status, doc_url\) | +| ↳ `forwardingNumber` | string | Forwarding number in E.164 format, or null | +| ↳ `healthStatus` | json | Line reputation/health status \(status, doc_url\) | ### `linq_list_thread` @@ -548,7 +551,7 @@ List all webhook subscriptions on your account ### `linq_mark_chat_read` -Mark all messages in a chat as read +Mark messages in a chat as read (only applies to 1:1 iMessage/RCS; no effect on group chats) #### Input @@ -785,3 +788,149 @@ Update a webhook subscription (target URL, events, phone filter, or active state | `updatedAt` | string | ISO 8601 update timestamp | + +## Triggers + +A **Trigger** is a block that starts a workflow when an event happens in this service. + +### Linq Message Delivered + +Trigger workflow when a message is delivered + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Message Failed + +Trigger workflow when a message fails to deliver + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Message Read + +Trigger workflow when a message is read + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Message Received + +Trigger workflow when an inbound message is received + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Reaction Added + +Trigger workflow when a reaction is added to a message + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + + +--- + +### Linq Webhook (All Events) + +Trigger on any Linq webhook event (messages, reactions, chats, and more) + +#### Configuration + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `eventType` | string | Event type \(e.g. message.received, message.delivered, reaction.added\) | +| `eventId` | string | Unique event identifier used for deduplication | +| `createdAt` | string | ISO 8601 timestamp of when the event occurred | +| `webhookVersion` | string | Payload schema version of the delivered event | +| `data` | json | Full event payload \(shape varies by event type — message, reaction, chat, etc.\) | + diff --git a/apps/sim/blocks/blocks/linq.ts b/apps/sim/blocks/blocks/linq.ts index 01dddba3942..e2ae7eaed95 100644 --- a/apps/sim/blocks/blocks/linq.ts +++ b/apps/sim/blocks/blocks/linq.ts @@ -2,6 +2,7 @@ import { LinqIcon } from '@/components/icons' import type { BlockConfig, BlockMeta } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' import { normalizeFileInput } from '@/blocks/utils' +import { getTrigger } from '@/triggers' const CHAT_ID_OPS = [ 'get_chat', @@ -559,6 +560,12 @@ export const LinqBlock: BlockConfig = { condition: { field: 'operation', value: [...PAGINATION_OPS] }, mode: 'advanced', }, + ...getTrigger('linq_message_received').subBlocks, + ...getTrigger('linq_message_delivered').subBlocks, + ...getTrigger('linq_message_failed').subBlocks, + ...getTrigger('linq_message_read').subBlocks, + ...getTrigger('linq_reaction_added').subBlocks, + ...getTrigger('linq_webhook').subBlocks, ], tools: { @@ -863,6 +870,18 @@ export const LinqBlock: BlockConfig = { events: { type: 'json', description: 'Available webhook event types' }, docUrl: { type: 'string', description: 'Documentation URL' }, }, + + triggers: { + enabled: true, + available: [ + 'linq_message_received', + 'linq_message_delivered', + 'linq_message_failed', + 'linq_message_read', + 'linq_reaction_added', + 'linq_webhook', + ], + }, } export const LinqBlockMeta = { diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 917a746c666..62ca382d8dc 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -10003,7 +10003,7 @@ }, { "name": "Mark Chat as Read", - "description": "Mark all messages in a chat as read" + "description": "Mark messages in a chat as read (only applies to 1:1 iMessage/RCS; no effect on group chats)" }, { "name": "Leave Chat", @@ -10119,8 +10119,39 @@ } ], "operationCount": 34, - "triggers": [], - "triggerCount": 0, + "triggers": [ + { + "id": "linq_message_received", + "name": "Linq Message Received", + "description": "Trigger workflow when an inbound message is received" + }, + { + "id": "linq_message_delivered", + "name": "Linq Message Delivered", + "description": "Trigger workflow when a message is delivered" + }, + { + "id": "linq_message_failed", + "name": "Linq Message Failed", + "description": "Trigger workflow when a message fails to deliver" + }, + { + "id": "linq_message_read", + "name": "Linq Message Read", + "description": "Trigger workflow when a message is read" + }, + { + "id": "linq_reaction_added", + "name": "Linq Reaction Added", + "description": "Trigger workflow when a reaction is added to a message" + }, + { + "id": "linq_webhook", + "name": "Linq Webhook (All Events)", + "description": "Trigger on any Linq webhook event (messages, reactions, chats, and more)" + } + ], + "triggerCount": 6, "authType": "api-key", "category": "tools", "integrationType": "communication", @@ -15649,11 +15680,11 @@ "landingContent": { "install": { "heading": "Add Sim to your Slack workspace", - "intro": "Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in) — connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.", + "intro": "Sim connects to Slack through Slack’s official OAuth flow. The “Add to Slack” button lives inside your Sim account (after sign-in). Connect from there and the Sim bot is installed in your Slack workspace. The steps below show exactly how to reach it.", "steps": [ { "title": "Create your free Sim account", - "body": "Sign up at sim.ai — no credit card required." + "body": "Sign up at sim.ai. No credit card required." }, { "title": "Add a Slack block", @@ -15673,7 +15704,7 @@ "body": "Sim requests only the Slack permissions its actions and triggers need, and never shows private channel names or messages to people who are not members of those channels in Slack.", "href": "/privacy" }, - "aiDisclaimer": "Sim agents use AI models to generate messages and responses sent to Slack. AI-generated content can be inaccurate or incomplete — review automated outputs before relying on them, especially for important communications." + "aiDisclaimer": "Sim agents use AI models to generate messages and responses sent to Slack. AI-generated content can be inaccurate or incomplete, so review automated outputs before relying on them, especially for important communications." } }, { diff --git a/apps/sim/lib/webhooks/providers/linq.ts b/apps/sim/lib/webhooks/providers/linq.ts new file mode 100644 index 00000000000..0d21ecf5900 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/linq.ts @@ -0,0 +1,266 @@ +import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Base64 } from '@sim/security/hmac' +import { NextResponse } from 'next/server' +import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import type { + AuthContext, + DeleteSubscriptionContext, + EventMatchContext, + FormatInputContext, + FormatInputResult, + SubscriptionContext, + SubscriptionResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' +import { LINQ_API_BASE, linqHeaders } from '@/tools/linq/utils' +import { LINQ_ALL_WEBHOOK_EVENT_TYPES, LINQ_TRIGGER_TO_EVENT_TYPE } from '@/triggers/linq/utils' + +const logger = createLogger('WebhookProvider:Linq') + +/** Max clock skew tolerated between the webhook timestamp and now (seconds). */ +const MAX_TIMESTAMP_SKEW_SECONDS = 5 * 60 + +/** + * Verify a Linq webhook signature using the Standard Webhooks scheme. + * Linq signs `${webhook-id}.${webhook-timestamp}.${rawBody}` with HMAC-SHA256 using + * the base64-decoded `whsec_...` signing secret, and delivers the result as one or + * more space-separated `v1,` signatures in the `webhook-signature` header. + */ +function verifyLinqSignature( + secret: string, + msgId: string, + timestamp: string, + signatures: string, + rawBody: string +): boolean { + try { + const ts = Number.parseInt(timestamp, 10) + const now = Math.floor(Date.now() / 1000) + if (Number.isNaN(ts) || Math.abs(now - ts) > MAX_TIMESTAMP_SKEW_SECONDS) { + return false + } + + const secretBytes = Buffer.from(secret.replace(/^whsec_/, ''), 'base64') + const toSign = `${msgId}.${timestamp}.${rawBody}` + const expectedSignature = hmacSha256Base64(toSign, secretBytes) + + for (const versionedSig of signatures.split(' ')) { + const parts = versionedSig.split(',') + if (parts.length !== 2) continue + if (safeCompare(parts[1], expectedSignature)) { + return true + } + } + return false + } catch (error) { + logger.error('Error verifying Linq webhook signature:', error) + return false + } +} + +/** Parse a comma/whitespace-separated list of phone numbers into a clean array. */ +function parsePhoneNumbers(value: unknown): string[] { + if (typeof value !== 'string') return [] + return value + .split(/[\n,]/) + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0) +} + +export const linqHandler: WebhookProviderHandler = { + async verifyAuth({ + request, + rawBody, + requestId, + providerConfig, + }: AuthContext): Promise { + const signingSecret = providerConfig.signingSecret as string | undefined + if (!signingSecret?.trim()) { + logger.warn(`[${requestId}] Linq webhook missing signing secret in provider configuration`) + return new NextResponse('Unauthorized - Linq signing secret is required', { status: 401 }) + } + + const webhookId = request.headers.get('webhook-id') + const webhookTimestamp = request.headers.get('webhook-timestamp') + const webhookSignature = request.headers.get('webhook-signature') + + if (!webhookId || !webhookTimestamp || !webhookSignature) { + logger.warn(`[${requestId}] Linq webhook missing Standard Webhooks signature headers`) + return new NextResponse('Unauthorized - Missing Linq signature headers', { status: 401 }) + } + + if ( + !verifyLinqSignature(signingSecret, webhookId, webhookTimestamp, webhookSignature, rawBody) + ) { + logger.warn(`[${requestId}] Linq webhook signature verification failed`) + return new NextResponse('Unauthorized - Invalid Linq signature', { status: 401 }) + } + + return null + }, + + matchEvent({ body, providerConfig, requestId }: EventMatchContext): boolean { + const triggerId = providerConfig.triggerId as string | undefined + if (!triggerId || triggerId === 'linq_webhook') { + return true + } + + const expectedType = LINQ_TRIGGER_TO_EVENT_TYPE[triggerId] + if (!expectedType) { + logger.debug(`[${requestId}] Unknown Linq triggerId ${triggerId}, skipping.`) + return false + } + + const actualType = (body as Record)?.event_type as string | undefined + if (actualType !== expectedType) { + logger.debug( + `[${requestId}] Linq event type mismatch: expected ${expectedType}, got ${actualType}. Skipping.` + ) + return false + } + + return true + }, + + async formatInput({ body }: FormatInputContext): Promise { + const payload = body as Record + return { + input: { + eventType: payload.event_type ?? null, + eventId: payload.event_id ?? null, + createdAt: payload.created_at ?? null, + webhookVersion: payload.webhook_version ?? null, + data: payload.data ?? null, + }, + } + }, + + extractIdempotencyId(body: unknown): string | null { + const eventId = (body as Record)?.event_id + return typeof eventId === 'string' && eventId.length > 0 ? eventId : null + }, + + async createSubscription(ctx: SubscriptionContext): Promise { + const { webhook, requestId } = ctx + const providerConfig = getProviderConfig(webhook) + const apiKey = providerConfig.apiKey as string | undefined + const triggerId = providerConfig.triggerId as string | undefined + + if (!apiKey) { + logger.warn(`[${requestId}] Missing apiKey for Linq webhook creation.`, { + webhookId: webhook.id, + }) + throw new Error( + 'Linq API Key is required. Please provide your Linq API Key in the trigger configuration.' + ) + } + + const events = + triggerId === 'linq_webhook' + ? LINQ_ALL_WEBHOOK_EVENT_TYPES + : triggerId && LINQ_TRIGGER_TO_EVENT_TYPE[triggerId] + ? [LINQ_TRIGGER_TO_EVENT_TYPE[triggerId]] + : null + + if (!events?.length) { + throw new Error(`Unknown or unsupported Linq trigger type: ${triggerId ?? '(missing)'}`) + } + + const phoneNumbers = parsePhoneNumbers(providerConfig.phoneNumbers) + const requestBody: Record = { + target_url: getNotificationUrl(webhook), + subscribed_events: events, + } + if (phoneNumbers.length > 0) { + requestBody.phone_numbers = phoneNumbers + } + + logger.info(`[${requestId}] Creating Linq webhook subscription`, { + triggerId, + events, + webhookId: webhook.id, + }) + + const response = await fetch(`${LINQ_API_BASE}/webhook-subscriptions`, { + method: 'POST', + headers: linqHeaders(apiKey), + body: JSON.stringify(requestBody), + }) + + const responseBody = (await response.json().catch(() => ({}))) as Record + + if (!response.ok) { + const errorMessage = + ((responseBody.error as Record)?.message as string) || + (responseBody.message as string) || + 'Unknown Linq API error' + logger.error( + `[${requestId}] Failed to create Linq webhook subscription for webhook ${webhook.id}. Status: ${response.status}`, + { message: errorMessage } + ) + + if (response.status === 401 || response.status === 403) { + throw new Error('Invalid Linq API Key. Please verify your API Key is correct.') + } + throw new Error(`Linq error: ${errorMessage}`) + } + + const externalId = responseBody.id + const signingSecret = responseBody.signing_secret + + if (typeof externalId !== 'string' || !externalId.trim()) { + throw new Error('Linq webhook was created but the API response did not include a webhook id.') + } + if (typeof signingSecret !== 'string' || !signingSecret.trim()) { + throw new Error( + 'Linq webhook was created but the API response did not include a signing secret.' + ) + } + + logger.info(`[${requestId}] Successfully created Linq webhook subscription ${externalId}.`) + + return { + providerConfigUpdates: { + externalId, + signingSecret, + }, + } + }, + + async deleteSubscription(ctx: DeleteSubscriptionContext): Promise { + const { webhook, requestId } = ctx + try { + const config = getProviderConfig(webhook) + const apiKey = config.apiKey as string | undefined + const externalId = config.externalId as string | undefined + + if (!apiKey || !externalId) { + logger.warn( + `[${requestId}] Missing apiKey or externalId for Linq webhook deletion ${webhook.id}, skipping cleanup` + ) + if (ctx.strict) throw new Error('Missing Linq webhook deletion credentials') + return + } + + const response = await fetch(`${LINQ_API_BASE}/webhook-subscriptions/${externalId}`, { + method: 'DELETE', + headers: linqHeaders(apiKey), + }) + + if (!response.ok && response.status !== 404) { + logger.warn( + `[${requestId}] Failed to delete Linq webhook subscription (non-fatal): ${response.status}` + ) + if (ctx.strict) { + throw new Error(`Failed to delete Linq webhook subscription: ${response.status}`) + } + } else { + logger.info(`[${requestId}] Successfully deleted Linq webhook subscription ${externalId}`) + } + } catch (error) { + logger.warn(`[${requestId}] Error deleting Linq webhook subscription (non-fatal)`, error) + if (ctx.strict) throw error + } + }, +} diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 93a96a6fc4e..1965a9663c7 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -28,6 +28,7 @@ import { jiraHandler } from '@/lib/webhooks/providers/jira' import { jsmHandler } from '@/lib/webhooks/providers/jsm' import { lemlistHandler } from '@/lib/webhooks/providers/lemlist' import { linearHandler } from '@/lib/webhooks/providers/linear' +import { linqHandler } from '@/lib/webhooks/providers/linq' import { loopsHandler } from '@/lib/webhooks/providers/loops' import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams' import { mondayHandler } from '@/lib/webhooks/providers/monday' @@ -88,6 +89,7 @@ const PROVIDER_HANDLERS: Record = { jsm: jsmHandler, lemlist: lemlistHandler, linear: linearHandler, + linq: linqHandler, loops: loopsHandler, monday: mondayHandler, resend: resendHandler, diff --git a/apps/sim/tools/linq/check_imessage.ts b/apps/sim/tools/linq/check_imessage.ts index cc44b5047b2..4718ce16490 100644 --- a/apps/sim/tools/linq/check_imessage.ts +++ b/apps/sim/tools/linq/check_imessage.ts @@ -44,7 +44,7 @@ export const linqCheckImessageTool: ToolConfig< }, transformResponse: async (response): Promise => { - const data = await response.json() + const data = await response.json().catch(() => null) if (!response.ok) { return { @@ -57,8 +57,8 @@ export const linqCheckImessageTool: ToolConfig< return { success: true, output: { - address: data.address ?? '', - available: data.available ?? false, + address: data?.address ?? '', + available: data?.available ?? false, }, } }, diff --git a/apps/sim/tools/linq/check_rcs.ts b/apps/sim/tools/linq/check_rcs.ts index 12c3808f871..582d7cc59e1 100644 --- a/apps/sim/tools/linq/check_rcs.ts +++ b/apps/sim/tools/linq/check_rcs.ts @@ -41,7 +41,7 @@ export const linqCheckRcsTool: ToolConfig => { - const data = await response.json() + const data = await response.json().catch(() => null) if (!response.ok) { return { @@ -54,8 +54,8 @@ export const linqCheckRcsTool: ToolConfig => { - const data = await response.json() + const data = await response.json().catch(() => null) if (!response.ok) { return { @@ -44,10 +44,12 @@ export const linqListPhoneNumbersTool: ToolConfig< return { success: true, output: { - phoneNumbers: (data.phone_numbers ?? []).map((num: Record) => ({ + phoneNumbers: (data?.phone_numbers ?? []).map((num: Record) => ({ id: (num.id as string) ?? '', phoneNumber: (num.phone_number as string) ?? '', - healthStatus: (num.health_status as LinqHealthStatus | undefined) ?? null, + forwardingNumber: (num.forwarding_number as string | null) ?? null, + healthStatus: + ((num.reputation ?? num.health_status) as LinqHealthStatus | undefined) ?? null, })), }, } @@ -62,7 +64,14 @@ export const linqListPhoneNumbersTool: ToolConfig< properties: { id: { type: 'string', description: 'Phone number ID' }, phoneNumber: { type: 'string', description: 'Phone number in E.164 format' }, - healthStatus: { type: 'json', description: 'Line health status (status, doc_url)' }, + forwardingNumber: { + type: 'string', + description: 'Forwarding number in E.164 format, or null', + }, + healthStatus: { + type: 'json', + description: 'Line reputation/health status (status, doc_url)', + }, }, }, }, diff --git a/apps/sim/tools/linq/mark_chat_read.ts b/apps/sim/tools/linq/mark_chat_read.ts index 25ea3e16c74..6ab72f68a39 100644 --- a/apps/sim/tools/linq/mark_chat_read.ts +++ b/apps/sim/tools/linq/mark_chat_read.ts @@ -5,7 +5,8 @@ import type { ToolConfig } from '@/tools/types' export const linqMarkChatReadTool: ToolConfig = { id: 'linq_mark_chat_read', name: 'Mark Chat as Read', - description: 'Mark all messages in a chat as read', + description: + 'Mark messages in a chat as read (only applies to 1:1 iMessage/RCS; no effect on group chats)', version: '1.0.0', params: { diff --git a/apps/sim/tools/linq/send_message.ts b/apps/sim/tools/linq/send_message.ts index f7448ece0d7..b417e9a5ce3 100644 --- a/apps/sim/tools/linq/send_message.ts +++ b/apps/sim/tools/linq/send_message.ts @@ -123,7 +123,8 @@ export const linqSendMessageTool: ToolConfig } diff --git a/apps/sim/triggers/linq/index.ts b/apps/sim/triggers/linq/index.ts new file mode 100644 index 00000000000..5ea641b6c0f --- /dev/null +++ b/apps/sim/triggers/linq/index.ts @@ -0,0 +1,6 @@ +export { linqMessageDeliveredTrigger } from './message_delivered' +export { linqMessageFailedTrigger } from './message_failed' +export { linqMessageReadTrigger } from './message_read' +export { linqMessageReceivedTrigger } from './message_received' +export { linqReactionAddedTrigger } from './reaction_added' +export { linqWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/linq/message_delivered.ts b/apps/sim/triggers/linq/message_delivered.ts new file mode 100644 index 00000000000..9328d818232 --- /dev/null +++ b/apps/sim/triggers/linq/message_delivered.ts @@ -0,0 +1,38 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Message Delivered Trigger + * Fires when a sent message is confirmed delivered to the recipient. + */ +export const linqMessageDeliveredTrigger: TriggerConfig = { + id: 'linq_message_delivered', + name: 'Linq Message Delivered', + provider: 'linq', + description: 'Trigger workflow when a message is delivered', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_message_delivered', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('message.delivered'), + extraFields: buildLinqExtraFields('linq_message_delivered'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/message_failed.ts b/apps/sim/triggers/linq/message_failed.ts new file mode 100644 index 00000000000..e5566e1edaa --- /dev/null +++ b/apps/sim/triggers/linq/message_failed.ts @@ -0,0 +1,38 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Message Failed Trigger + * Fires when a sent message fails to deliver. + */ +export const linqMessageFailedTrigger: TriggerConfig = { + id: 'linq_message_failed', + name: 'Linq Message Failed', + provider: 'linq', + description: 'Trigger workflow when a message fails to deliver', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_message_failed', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('message.failed'), + extraFields: buildLinqExtraFields('linq_message_failed'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/message_read.ts b/apps/sim/triggers/linq/message_read.ts new file mode 100644 index 00000000000..7280f945b3b --- /dev/null +++ b/apps/sim/triggers/linq/message_read.ts @@ -0,0 +1,38 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Message Read Trigger + * Fires when a recipient reads a sent message (1:1 iMessage/RCS). + */ +export const linqMessageReadTrigger: TriggerConfig = { + id: 'linq_message_read', + name: 'Linq Message Read', + provider: 'linq', + description: 'Trigger workflow when a message is read', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_message_read', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('message.read'), + extraFields: buildLinqExtraFields('linq_message_read'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/message_received.ts b/apps/sim/triggers/linq/message_received.ts new file mode 100644 index 00000000000..92a4edb156a --- /dev/null +++ b/apps/sim/triggers/linq/message_received.ts @@ -0,0 +1,39 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Message Received Trigger + * Fires when an inbound iMessage, SMS, or RCS message arrives. + */ +export const linqMessageReceivedTrigger: TriggerConfig = { + id: 'linq_message_received', + name: 'Linq Message Received', + provider: 'linq', + description: 'Trigger workflow when an inbound message is received', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_message_received', + triggerOptions: linqTriggerOptions, + includeDropdown: true, + setupInstructions: linqSetupInstructions('message.received'), + extraFields: buildLinqExtraFields('linq_message_received'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/reaction_added.ts b/apps/sim/triggers/linq/reaction_added.ts new file mode 100644 index 00000000000..5d9cb55e0f0 --- /dev/null +++ b/apps/sim/triggers/linq/reaction_added.ts @@ -0,0 +1,38 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Linq Reaction Added Trigger + * Fires when a tapback or custom reaction is added to a message. + */ +export const linqReactionAddedTrigger: TriggerConfig = { + id: 'linq_reaction_added', + name: 'Linq Reaction Added', + provider: 'linq', + description: 'Trigger workflow when a reaction is added to a message', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_reaction_added', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('reaction.added'), + extraFields: buildLinqExtraFields('linq_reaction_added'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/linq/utils.ts b/apps/sim/triggers/linq/utils.ts new file mode 100644 index 00000000000..f8af0317de8 --- /dev/null +++ b/apps/sim/triggers/linq/utils.ts @@ -0,0 +1,143 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +/** + * Maps Sim Linq trigger IDs to a single Linq webhook event type. + * Kept in sync with subscription registration in the `linq` webhook provider. + */ +export const LINQ_TRIGGER_TO_EVENT_TYPE: Record = { + linq_message_received: 'message.received', + linq_message_delivered: 'message.delivered', + linq_message_failed: 'message.failed', + linq_message_read: 'message.read', + linq_reaction_added: 'reaction.added', +} + +/** + * Every Linq webhook event type, registered for the catch-all `linq_webhook` trigger. + * Mirrors the Linq OpenAPI `WebhookEventType` enum verbatim (all 27 values). + */ +export const LINQ_ALL_WEBHOOK_EVENT_TYPES: string[] = [ + 'message.sent', + 'message.received', + 'message.read', + 'message.delivered', + 'message.failed', + 'message.edited', + 'reaction.added', + 'reaction.removed', + 'participant.added', + 'participant.removed', + 'chat.created', + 'chat.group_name_updated', + 'chat.group_icon_updated', + 'chat.group_name_update_failed', + 'chat.group_icon_update_failed', + 'chat.typing_indicator.started', + 'chat.typing_indicator.stopped', + 'phone_number.status_updated', + 'call.initiated', + 'call.ringing', + 'call.answered', + 'call.ended', + 'call.failed', + 'call.declined', + 'call.no_answer', + 'location.sharing.started', + 'location.sharing.stopped', +] + +/** Shared trigger dropdown options for all Linq triggers. */ +export const linqTriggerOptions = [ + { label: 'Message Received', id: 'linq_message_received' }, + { label: 'Message Delivered', id: 'linq_message_delivered' }, + { label: 'Message Failed', id: 'linq_message_failed' }, + { label: 'Message Read', id: 'linq_message_read' }, + { label: 'Reaction Added', id: 'linq_reaction_added' }, + { label: 'Webhook (All Events)', id: 'linq_webhook' }, +] + +/** + * Generates setup instructions for Linq webhooks. + * The subscription is created and deleted automatically via the Linq API. + */ +export function linqSetupInstructions(eventType: string): string { + const instructions = [ + 'Enter your Linq API Key above.', + 'You can find your API key in the Linq partner dashboard.', + 'Optionally restrict delivery to specific phone numbers (E.164, comma-separated). Leave empty to receive events from all numbers.', + `Click "Save Configuration" to automatically create the webhook subscription in Linq for ${eventType}.`, + 'The subscription is automatically deleted when you remove this trigger.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Builds Linq-specific extra fields for the trigger UI. + * Includes the required API key and an optional phone-number filter. + * Use with the generic `buildTriggerSubBlocks` from `@/triggers`. + */ +export function buildLinqExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your Linq API key', + description: 'Required to create the webhook subscription in Linq.', + password: true, + paramVisibility: 'user-only', + required: true, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'phoneNumbers', + title: 'Phone Numbers (Optional)', + type: 'short-input', + placeholder: 'Leave empty for all numbers (e.g. +15551234567, +15557654321)', + description: 'Comma-separated E.164 numbers to restrict which numbers deliver events.', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +/** + * Outputs exposed by every Linq trigger. + * + * Only the delivery-envelope fields are documented in the Linq OpenAPI spec; the + * per-event `data` shape is not enumerated, so it is surfaced as a JSON passthrough + * rather than fabricating typed sub-fields. + */ +export function buildLinqOutputs(): Record { + return { + eventType: { + type: 'string', + description: 'Event type (e.g. message.received, message.delivered, reaction.added)', + }, + eventId: { + type: 'string', + description: 'Unique event identifier used for deduplication', + }, + createdAt: { + type: 'string', + description: 'ISO 8601 timestamp of when the event occurred', + }, + webhookVersion: { + type: 'string', + description: 'Payload schema version of the delivered event', + }, + data: { + type: 'json', + description: + 'Full event payload (shape varies by event type — message, reaction, chat, etc.)', + }, + } +} diff --git a/apps/sim/triggers/linq/webhook.ts b/apps/sim/triggers/linq/webhook.ts new file mode 100644 index 00000000000..d7334d3c349 --- /dev/null +++ b/apps/sim/triggers/linq/webhook.ts @@ -0,0 +1,40 @@ +import { LinqIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildLinqExtraFields, + buildLinqOutputs, + linqSetupInstructions, + linqTriggerOptions, +} from '@/triggers/linq/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * Generic Linq Webhook Trigger + * Subscribes to every Linq webhook event type (messages, reactions, participants, + * chats, typing, phone number status, location sharing). Use the data + * output for the full payload, which varies by eventType. + */ +export const linqWebhookTrigger: TriggerConfig = { + id: 'linq_webhook', + name: 'Linq Webhook (All Events)', + provider: 'linq', + description: 'Trigger on any Linq webhook event (messages, reactions, chats, and more)', + version: '1.0.0', + icon: LinqIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'linq_webhook', + triggerOptions: linqTriggerOptions, + setupInstructions: linqSetupInstructions('All Events'), + extraFields: buildLinqExtraFields('linq_webhook'), + }), + + outputs: buildLinqOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 3869d8a66bc..c13704ed89f 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -267,6 +267,14 @@ import { linearWebhookTrigger, linearWebhookV2Trigger, } from '@/triggers/linear' +import { + linqMessageDeliveredTrigger, + linqMessageFailedTrigger, + linqMessageReadTrigger, + linqMessageReceivedTrigger, + linqReactionAddedTrigger, + linqWebhookTrigger, +} from '@/triggers/linq' import { loopsCampaignEmailSentTrigger, loopsEmailClickedTrigger, @@ -598,6 +606,12 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { linear_project_update_created_v2: linearProjectUpdateCreatedV2Trigger, linear_customer_request_created_v2: linearCustomerRequestCreatedV2Trigger, linear_customer_request_updated_v2: linearCustomerRequestUpdatedV2Trigger, + linq_message_received: linqMessageReceivedTrigger, + linq_message_delivered: linqMessageDeliveredTrigger, + linq_message_failed: linqMessageFailedTrigger, + linq_message_read: linqMessageReadTrigger, + linq_reaction_added: linqReactionAddedTrigger, + linq_webhook: linqWebhookTrigger, monday_item_created: mondayItemCreatedTrigger, monday_column_changed: mondayColumnChangedTrigger, monday_status_changed: mondayStatusChangedTrigger, From 88e9b34497b9cc8d56ac132bd8ea85c738e0a97f Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 16:38:30 -0700 Subject: [PATCH 2/6] refactor(linq): drop phantom create_chat response path, complete deliveryStatus enum doc Final validation against the raw Linq OpenAPI spec confirmed the sent message is at chat.message (CreateChatResult exposes only chat), so the data.message fallback was dead code. Also list all 7 DeliveryStatus values in the send_message output description. --- apps/sim/tools/linq/create_chat.ts | 2 +- apps/sim/tools/linq/send_message.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/linq/create_chat.ts b/apps/sim/tools/linq/create_chat.ts index c188cf75fea..5b57c44a167 100644 --- a/apps/sim/tools/linq/create_chat.ts +++ b/apps/sim/tools/linq/create_chat.ts @@ -136,7 +136,7 @@ export const linqCreateChatTool: ToolConfig Date: Tue, 30 Jun 2026 16:54:08 -0700 Subject: [PATCH 3/6] fix(linq): namespace trigger credential keys, handle edit_message 204, nullable forwardingNumber MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review + final pre-merge audit fixes: - Trigger apiKey/phoneNumbers subblocks collided with the block's tool apiKey state key — rename to triggerApiKey/triggerPhoneNumbers (per the namespacing rule from #2133) and read them in the webhook handler - edit_message: the API returns 204 No Content when editing an already-deleted message; guard the empty body instead of throwing on response.json() - list_phone_numbers: mark forwardingNumber output nullable (returns null) - check_rcs: tighten address hint (RCS is phone-only, not email) - regenerated docs --- .../content/docs/en/integrations/linq.mdx | 28 +++++++++--------- apps/sim/lib/webhooks/providers/linq.ts | 4 +-- apps/sim/tools/linq/check_rcs.ts | 2 +- apps/sim/tools/linq/edit_message.ts | 29 ++++++++++--------- apps/sim/tools/linq/list_phone_numbers.ts | 1 + apps/sim/triggers/linq/utils.ts | 4 +-- 6 files changed, 36 insertions(+), 32 deletions(-) diff --git a/apps/docs/content/docs/en/integrations/linq.mdx b/apps/docs/content/docs/en/integrations/linq.mdx index 7f4c035589b..dad202058e7 100644 --- a/apps/docs/content/docs/en/integrations/linq.mdx +++ b/apps/docs/content/docs/en/integrations/linq.mdx @@ -88,7 +88,7 @@ Check whether an address (phone number or email) supports RCS | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `apiKey` | string | Yes | Linq API key | -| `address` | string | Yes | Phone number \(E.164 format\) or email address to check | +| `address` | string | Yes | Phone number \(E.164 format\) to check | | `from` | string | No | Sender phone number to check from \(defaults to an available number\) | #### Output @@ -636,7 +636,7 @@ Send a message to an existing chat, with optional media, link, effect, or reply | --------- | ---- | ----------- | | `chatId` | string | ID of the chat the message was sent to | | `messageId` | string | ID of the sent message | -| `deliveryStatus` | string | Delivery status \(pending, queued, sent, delivered, failed\) | +| `deliveryStatus` | string | Delivery status \(pending, queued, sent, delivered, received, read, failed\) | | `sentAt` | string | ISO 8601 timestamp the message was sent | | `service` | string | Delivery service \(iMessage, SMS, RCS\) | | `message` | json | The full sent message object with parts | @@ -801,8 +801,8 @@ Trigger workflow when a message is delivered | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | -| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | #### Output @@ -825,8 +825,8 @@ Trigger workflow when a message fails to deliver | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | -| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | #### Output @@ -849,8 +849,8 @@ Trigger workflow when a message is read | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | -| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | #### Output @@ -873,8 +873,8 @@ Trigger workflow when an inbound message is received | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | -| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | #### Output @@ -897,8 +897,8 @@ Trigger workflow when a reaction is added to a message | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | -| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | #### Output @@ -921,8 +921,8 @@ Trigger on any Linq webhook event (messages, reactions, chats, and more) | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `apiKey` | string | Yes | Required to create the webhook subscription in Linq. | -| `phoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | +| `triggerApiKey` | string | Yes | Required to create the webhook subscription in Linq. | +| `triggerPhoneNumbers` | string | No | Comma-separated E.164 numbers to restrict which numbers deliver events. | #### Output diff --git a/apps/sim/lib/webhooks/providers/linq.ts b/apps/sim/lib/webhooks/providers/linq.ts index 0d21ecf5900..5d7215f1595 100644 --- a/apps/sim/lib/webhooks/providers/linq.ts +++ b/apps/sim/lib/webhooks/providers/linq.ts @@ -144,7 +144,7 @@ export const linqHandler: WebhookProviderHandler = { async createSubscription(ctx: SubscriptionContext): Promise { const { webhook, requestId } = ctx const providerConfig = getProviderConfig(webhook) - const apiKey = providerConfig.apiKey as string | undefined + const apiKey = providerConfig.triggerApiKey as string | undefined const triggerId = providerConfig.triggerId as string | undefined if (!apiKey) { @@ -167,7 +167,7 @@ export const linqHandler: WebhookProviderHandler = { throw new Error(`Unknown or unsupported Linq trigger type: ${triggerId ?? '(missing)'}`) } - const phoneNumbers = parsePhoneNumbers(providerConfig.phoneNumbers) + const phoneNumbers = parsePhoneNumbers(providerConfig.triggerPhoneNumbers) const requestBody: Record = { target_url: getNotificationUrl(webhook), subscribed_events: events, diff --git a/apps/sim/tools/linq/check_rcs.ts b/apps/sim/tools/linq/check_rcs.ts index 582d7cc59e1..84993b2ae1f 100644 --- a/apps/sim/tools/linq/check_rcs.ts +++ b/apps/sim/tools/linq/check_rcs.ts @@ -19,7 +19,7 @@ export const linqCheckRcsTool: ToolConfig => { - const data = await response.json() + // Linq returns 200 with the updated Message, or 204 No Content when the edit + // is accepted for an already-deleted message — guard the empty-body case. + const data = await response.json().catch(() => null) if (!response.ok) { return { @@ -71,21 +73,22 @@ export const linqEditMessageTool: ToolConfig Date: Tue, 30 Jun 2026 17:06:24 -0700 Subject: [PATCH 4/6] fix(linq): read triggerApiKey in webhook deleteSubscription deleteSubscription still read config.apiKey after the credential rename, so undeploy would skip the DELETE and orphan the Linq subscription. Match createSubscription's triggerApiKey key. --- apps/sim/lib/webhooks/providers/linq.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/webhooks/providers/linq.ts b/apps/sim/lib/webhooks/providers/linq.ts index 5d7215f1595..819b87ce0c7 100644 --- a/apps/sim/lib/webhooks/providers/linq.ts +++ b/apps/sim/lib/webhooks/providers/linq.ts @@ -232,7 +232,7 @@ export const linqHandler: WebhookProviderHandler = { const { webhook, requestId } = ctx try { const config = getProviderConfig(webhook) - const apiKey = config.apiKey as string | undefined + const apiKey = config.triggerApiKey as string | undefined const externalId = config.externalId as string | undefined if (!apiKey || !externalId) { From 4a7fc217f7026a7570eecdd88081d987097449ba Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 17:11:21 -0700 Subject: [PATCH 5/6] fix(linq): mark list_phone_numbers healthStatus output nullable healthStatus returns null when Linq omits reputation/health_status; declare nullable: true to match the runtime value (same as forwardingNumber). --- apps/sim/tools/linq/list_phone_numbers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/tools/linq/list_phone_numbers.ts b/apps/sim/tools/linq/list_phone_numbers.ts index 5b563b4c0a1..303737c0a0e 100644 --- a/apps/sim/tools/linq/list_phone_numbers.ts +++ b/apps/sim/tools/linq/list_phone_numbers.ts @@ -72,6 +72,7 @@ export const linqListPhoneNumbersTool: ToolConfig< healthStatus: { type: 'json', description: 'Line reputation/health status (status, doc_url)', + nullable: true, }, }, }, From 1f8c0ef5db08880bf718df22656f8282640591e3 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 30 Jun 2026 17:13:00 -0700 Subject: [PATCH 6/6] fix(linq): mark nullable list_webhook_subscriptions item fields phoneNumbers/createdAt/updatedAt are null-coerced by mapWebhookSubscription; declare nullable: true on the array-item schema to match runtime (consistent with the top-level webhook outputs' optional flags). --- .../tools/linq/list_webhook_subscriptions.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/sim/tools/linq/list_webhook_subscriptions.ts b/apps/sim/tools/linq/list_webhook_subscriptions.ts index 391c6680e73..c9c7ec4bd3a 100644 --- a/apps/sim/tools/linq/list_webhook_subscriptions.ts +++ b/apps/sim/tools/linq/list_webhook_subscriptions.ts @@ -63,10 +63,22 @@ export const linqListWebhookSubscriptionsTool: ToolConfig< id: { type: 'string', description: 'Subscription ID' }, targetUrl: { type: 'string', description: 'Endpoint that receives events' }, subscribedEvents: { type: 'json', description: 'Subscribed event types' }, - phoneNumbers: { type: 'json', description: 'Filtered phone numbers (null = all)' }, + phoneNumbers: { + type: 'json', + description: 'Filtered phone numbers (null = all)', + nullable: true, + }, isActive: { type: 'boolean', description: 'Whether the subscription is active' }, - createdAt: { type: 'string', description: 'ISO 8601 creation timestamp' }, - updatedAt: { type: 'string', description: 'ISO 8601 update timestamp' }, + createdAt: { + type: 'string', + description: 'ISO 8601 creation timestamp', + nullable: true, + }, + updatedAt: { + type: 'string', + description: 'ISO 8601 update timestamp', + nullable: true, + }, }, }, },