From e9455474a62b2a38dd0a6cb77b841b9eaddeeab5 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 8 Jun 2026 18:33:42 -0700 Subject: [PATCH 1/6] feat(sendblue): add Sendblue iMessage/SMS integration with tools and triggers --- apps/docs/components/icons.tsx | 15 + apps/docs/components/ui/icon-mapping.ts | 2 + apps/docs/content/docs/en/tools/meta.json | 1 + apps/docs/content/docs/en/tools/sendblue.mdx | 198 +++++++++ apps/docs/content/docs/en/triggers/meta.json | 1 + .../content/docs/en/triggers/sendblue.mdx | 93 +++++ apps/sim/blocks/blocks/sendblue.ts | 382 ++++++++++++++++++ apps/sim/blocks/registry.ts | 3 + apps/sim/components/icons.tsx | 15 + apps/sim/lib/integrations/icon-mapping.ts | 2 + apps/sim/lib/integrations/integrations.json | 50 +++ apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/lib/webhooks/providers/sendblue.ts | 88 ++++ apps/sim/tools/registry.ts | 12 + apps/sim/tools/sendblue/evaluate_service.ts | 56 +++ apps/sim/tools/sendblue/get_message.ts | 130 ++++++ apps/sim/tools/sendblue/index.ts | 6 + apps/sim/tools/sendblue/send_group_message.ts | 165 ++++++++ apps/sim/tools/sendblue/send_message.ts | 141 +++++++ .../tools/sendblue/send_typing_indicator.ts | 75 ++++ apps/sim/tools/sendblue/types.ts | 136 +++++++ apps/sim/tools/sendblue/utils.ts | 38 ++ apps/sim/triggers/registry.ts | 6 + apps/sim/triggers/sendblue/index.ts | 2 + .../sim/triggers/sendblue/message_received.ts | 25 ++ .../sendblue/message_status_updated.ts | 25 ++ apps/sim/triggers/sendblue/utils.ts | 85 ++++ 27 files changed, 1754 insertions(+) create mode 100644 apps/docs/content/docs/en/tools/sendblue.mdx create mode 100644 apps/docs/content/docs/en/triggers/sendblue.mdx create mode 100644 apps/sim/blocks/blocks/sendblue.ts create mode 100644 apps/sim/lib/webhooks/providers/sendblue.ts create mode 100644 apps/sim/tools/sendblue/evaluate_service.ts create mode 100644 apps/sim/tools/sendblue/get_message.ts create mode 100644 apps/sim/tools/sendblue/index.ts create mode 100644 apps/sim/tools/sendblue/send_group_message.ts create mode 100644 apps/sim/tools/sendblue/send_message.ts create mode 100644 apps/sim/tools/sendblue/send_typing_indicator.ts create mode 100644 apps/sim/tools/sendblue/types.ts create mode 100644 apps/sim/tools/sendblue/utils.ts create mode 100644 apps/sim/triggers/sendblue/index.ts create mode 100644 apps/sim/triggers/sendblue/message_received.ts create mode 100644 apps/sim/triggers/sendblue/message_status_updated.ts create mode 100644 apps/sim/triggers/sendblue/utils.ts diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 3a887ccd6ae..6856938a1f8 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -2047,6 +2047,21 @@ export function TwilioIcon(props: SVGProps) { ) } +export function SendblueIcon(props: SVGProps) { + return ( + + + + + ) +} + export function ImageIcon(props: SVGProps) { return ( = { sap_concur: SapConcurIcon, sap_s4hana: SapS4HanaIcon, secrets_manager: SecretsManagerIcon, + sendblue: SendblueIcon, sendgrid: SendgridIcon, sentry: SentryIcon, serper: SerperIcon, diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index cda4afe49b5..c1e0a1d2081 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -157,6 +157,7 @@ "sap_concur", "sap_s4hana", "secrets_manager", + "sendblue", "sendgrid", "sentry", "serper", diff --git a/apps/docs/content/docs/en/tools/sendblue.mdx b/apps/docs/content/docs/en/tools/sendblue.mdx new file mode 100644 index 00000000000..f25735c70b6 --- /dev/null +++ b/apps/docs/content/docs/en/tools/sendblue.mdx @@ -0,0 +1,198 @@ +--- +title: Sendblue +description: Send and receive iMessage and SMS +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +## Manual Description + +Sendblue connects your agents to iMessage and SMS through your own dedicated phone number. Use it to text one person or a group, attach images and other media, check whether a number can receive iMessage before you send, show a typing indicator, and look up the delivery status of any message. + +Authentication uses a Sendblue **API Key ID** and **API Secret Key**, sent as the `sb-api-key-id` and `sb-api-secret-key` headers. You can find both in your [Sendblue dashboard](https://dashboard.sendblue.com). Every message is sent from one of your registered Sendblue lines, supplied as the **From Number** in E.164 format (for example `+15551234567`). + +**Operations** + +- **Send Message** — send an iMessage or SMS to a single recipient. Provide message text, a media URL, or both, and optionally apply an iMessage expressive style (celebration, fireworks, lasers, confetti, and more). +- **Send Group Message** — send to multiple recipients at once. Pass one recipient per line; reuse the returned `group_id` to keep replying in the same thread. +- **Evaluate Service** — check whether a number is reachable on iMessage or only SMS, so you can branch before sending. +- **Send Typing Indicator** — show a recipient that a reply is being composed (one-to-one chats only). +- **Get Message** — retrieve a single message and its current status by message handle. + +**Triggers** + +- **Message Received** — fires on every inbound message. Configure it as the **Receive** webhook in your Sendblue dashboard. +- **Message Status Updated** — fires when an outbound message changes state (`SENT`, `DELIVERED`, `ERROR`). Configure it as the **Outbound** webhook, or pass its URL per message as `status_callback`. + +Each trigger generates its own webhook URL — paste the matching URL into the corresponding field in your Sendblue dashboard. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Send iMessages and SMS to individuals or groups, check whether a number supports iMessage, show typing indicators, and look up message status with Sendblue. Trigger workflows on inbound messages and delivery status updates. + + + +## Tools + +### `sendblue_send_message` + +Send an iMessage or SMS to a single recipient via Sendblue. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `number` | string | Yes | Recipient phone number in E.164 format \(e.g., +19998887777\) | +| `from_number` | string | Yes | One of your registered Sendblue phone numbers to send from, in E.164 format \(e.g., +18887776666\) | +| `content` | string | No | Message text content. Either content or media_url must be provided. | +| `media_url` | string | No | URL of a media file to send. Either content or media_url must be provided. | +| `send_style` | string | No | iMessage expressive style \(e.g., celebration, fireworks, lasers, confetti, balloons, invisible, slam\). | +| `status_callback` | string | No | Webhook URL that Sendblue will POST message status updates to. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Message status: QUEUED, SENT, DELIVERED, or ERROR | +| `message_handle` | string | Unique identifier for tracking the message | +| `account_email` | string | Email of the account that sent the message | +| `content` | string | Message content | +| `is_outbound` | boolean | Whether this is an outbound message | +| `from_number` | string | Sending phone number | +| `number` | string | Recipient phone number | +| `media_url` | string | URL of attached media | +| `send_style` | string | iMessage expressive style applied | +| `seat_id` | string | UUID of the seat that sent the message | +| `sender_email` | string | Email of the seat \(user\) that sent the message | +| `error_code` | number | Numeric error code if the message failed | +| `error_message` | string | Error message if the message failed | +| `date_created` | string | When the message was created | +| `date_updated` | string | When the message was last updated | + +### `sendblue_send_group_message` + +Send an iMessage or SMS to a group of recipients via Sendblue. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `numbers` | array | Yes | Recipient phone numbers in E.164 format \(e.g., \["+19998887777", "+13334445555"\]\) | +| `items` | string | No | No description | +| `from_number` | string | Yes | One of your registered Sendblue phone numbers to send from, in E.164 format \(e.g., +18887776666\) | +| `content` | string | No | Message text content. Either content or media_url must be provided. | +| `media_url` | string | No | URL of a media file to send. Either content or media_url must be provided. | +| `send_style` | string | No | iMessage expressive style \(e.g., celebration, fireworks, lasers, confetti, balloons, invisible, slam\). | +| `group_id` | string | No | Unique identifier of an existing group to send to. Omit to start a new group. | +| `status_callback` | string | No | Webhook URL that Sendblue will POST message status updates to. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Message status: QUEUED, SENT, DELIVERED, or ERROR | +| `message_handle` | string | Unique identifier for tracking the message | +| `group_id` | string | Identifier of the group the message was sent to | +| `participants` | array | Phone numbers participating in the group | +| `account_email` | string | Email of the account that sent the message | +| `content` | string | Message content | +| `is_outbound` | boolean | Whether this is an outbound message | +| `from_number` | string | Sending phone number | +| `number` | string | Recipient phone number | +| `media_url` | string | URL of attached media | +| `send_style` | string | iMessage expressive style applied | +| `seat_id` | string | UUID of the seat that sent the message | +| `sender_email` | string | Email of the seat \(user\) that sent the message | +| `error_code` | number | Numeric error code if the message failed | +| `error_message` | string | Error message if the message failed | +| `date_created` | string | When the message was created | +| `date_updated` | string | When the message was last updated | + +### `sendblue_evaluate_service` + +Check whether a phone number can receive iMessage or only SMS. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `number` | string | Yes | Phone number to evaluate, in E.164 format \(e.g., +19998887777\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `number` | string | The evaluated phone number in E.164 format | +| `service` | string | The service the number supports: iMessage or SMS | + +### `sendblue_send_typing_indicator` + +Display a typing indicator to a recipient (not supported in group chats). + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `number` | string | Yes | Recipient's phone number in E.164 format \(e.g., +19998887777\) | +| `from_number` | string | No | Your Sendblue line number to send from, in E.164 format. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Delivery status of the typing indicator \(e.g., QUEUED\) | +| `status_code` | number | Numeric status code returned by Sendblue | +| `number` | string | The recipient phone number | +| `error_message` | string | Error details, null on success | + +### `sendblue_get_message` + +Retrieve a single message and its current status by message handle/ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `message_id` | string | Yes | The message handle/ID returned when the message was sent. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Current message status | +| `message_handle` | string | Unique message identifier | +| `account_email` | string | Email of the account | +| `content` | string | Message content | +| `is_outbound` | boolean | Whether the message is outbound | +| `from_number` | string | Sending phone number | +| `number` | string | Recipient phone number | +| `to_number` | string | Destination phone number | +| `media_url` | string | URL of attached media | +| `message_type` | string | Message category: message or group | +| `service` | string | Messaging service: iMessage, SMS, or RCS | +| `group_id` | string | Group identifier \(empty for non-group\) | +| `group_display_name` | string | Group chat name | +| `participants` | array | Participant phone numbers | +| `send_style` | string | Expressive style applied | +| `was_downgraded` | boolean | True if the recipient lacks iMessage support | +| `opted_out` | boolean | True if the recipient has opted out | +| `plan` | string | Account plan type | +| `sendblue_number` | string | Sendblue phone number used | +| `seat_id` | string | Seat UUID | +| `sender_email` | string | Email of the sending seat | +| `error_code` | number | Numeric error code if failed | +| `error_message` | string | Error message if failed | +| `error_reason` | string | Additional error context | +| `error_detail` | string | Detailed error information | +| `date_sent` | string | ISO 8601 creation timestamp | +| `date_updated` | string | ISO 8601 last-update timestamp | + + diff --git a/apps/docs/content/docs/en/triggers/meta.json b/apps/docs/content/docs/en/triggers/meta.json index ab14483dd52..10a41e72ef0 100644 --- a/apps/docs/content/docs/en/triggers/meta.json +++ b/apps/docs/content/docs/en/triggers/meta.json @@ -38,6 +38,7 @@ "outlook", "resend", "salesforce", + "sendblue", "servicenow", "slack", "stripe", diff --git a/apps/docs/content/docs/en/triggers/sendblue.mdx b/apps/docs/content/docs/en/triggers/sendblue.mdx new file mode 100644 index 00000000000..79a94101aeb --- /dev/null +++ b/apps/docs/content/docs/en/triggers/sendblue.mdx @@ -0,0 +1,93 @@ +--- +title: Sendblue +description: Available Sendblue triggers for automating workflows +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +Sendblue provides 2 triggers for automating workflows based on events. + +## Triggers + +### Sendblue Message Received + +Trigger when an inbound iMessage or SMS is received in Sendblue + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `accountEmail` | string | Email of the Sendblue account | +| `content` | string | Message text content | +| `media_url` | string | CDN link to attached media, if any | +| `is_outbound` | boolean | True for outbound messages, false for inbound | +| `status` | string | Message status \(e.g., RECEIVED, QUEUED, SENT, DELIVERED, ERROR\) | +| `error_code` | number | Error identifier, null if none | +| `error_message` | string | Descriptive error text, null if none | +| `error_reason` | string | Additional error context, null if none | +| `error_detail` | string | Detailed error information, null if none | +| `message_handle` | string | Sendblue message identifier \(use to deduplicate\) | +| `date_sent` | string | ISO 8601 creation timestamp | +| `date_updated` | string | ISO 8601 last-update timestamp | +| `from_number` | string | E.164 sender phone number | +| `number` | string | E.164 recipient/counterparty phone number | +| `to_number` | string | E.164 destination phone number | +| `was_downgraded` | boolean | True if the recipient lacks iMessage support | +| `plan` | string | Account plan type | +| `message_type` | string | Message category \(e.g., message, group\) | +| `group_id` | string | Group identifier, empty for non-group messages | +| `participants` | array | Participant phone numbers for group messages | +| `send_style` | string | Expressive style if applied | +| `opted_out` | boolean | True if the recipient has opted out | +| `sendblue_number` | string | Sendblue phone number used | +| `service` | string | Messaging service \(iMessage or SMS\) | +| `group_display_name` | string | Group chat name, null for non-group messages | +| `sender_email` | string | Email of the user who sent the message | +| `seat_id` | string | Seat UUID, null if absent | +| `raw` | string | Complete raw webhook payload from Sendblue as a JSON string | + + +--- + +### Sendblue Message Status Updated + +Trigger when an outbound message status changes (SENT, DELIVERED, ERROR) in Sendblue + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `accountEmail` | string | Email of the Sendblue account | +| `content` | string | Message text content | +| `media_url` | string | CDN link to attached media, if any | +| `is_outbound` | boolean | True for outbound messages, false for inbound | +| `status` | string | Message status \(e.g., RECEIVED, QUEUED, SENT, DELIVERED, ERROR\) | +| `error_code` | number | Error identifier, null if none | +| `error_message` | string | Descriptive error text, null if none | +| `error_reason` | string | Additional error context, null if none | +| `error_detail` | string | Detailed error information, null if none | +| `message_handle` | string | Sendblue message identifier \(use to deduplicate\) | +| `date_sent` | string | ISO 8601 creation timestamp | +| `date_updated` | string | ISO 8601 last-update timestamp | +| `from_number` | string | E.164 sender phone number | +| `number` | string | E.164 recipient/counterparty phone number | +| `to_number` | string | E.164 destination phone number | +| `was_downgraded` | boolean | True if the recipient lacks iMessage support | +| `plan` | string | Account plan type | +| `message_type` | string | Message category \(e.g., message, group\) | +| `group_id` | string | Group identifier, empty for non-group messages | +| `participants` | array | Participant phone numbers for group messages | +| `send_style` | string | Expressive style if applied | +| `opted_out` | boolean | True if the recipient has opted out | +| `sendblue_number` | string | Sendblue phone number used | +| `service` | string | Messaging service \(iMessage or SMS\) | +| `group_display_name` | string | Group chat name, null for non-group messages | +| `sender_email` | string | Email of the user who sent the message | +| `seat_id` | string | Seat UUID, null if absent | +| `raw` | string | Complete raw webhook payload from Sendblue as a JSON string | + diff --git a/apps/sim/blocks/blocks/sendblue.ts b/apps/sim/blocks/blocks/sendblue.ts new file mode 100644 index 00000000000..9b4a105ec5e --- /dev/null +++ b/apps/sim/blocks/blocks/sendblue.ts @@ -0,0 +1,382 @@ +import { SendblueIcon } from '@/components/icons' +import type { BlockConfig, BlockMeta } from '@/blocks/types' +import { AuthMode, IntegrationType } from '@/blocks/types' +import { getTrigger } from '@/triggers' + +const SEND_STYLE_OPTIONS = [ + { label: 'None', id: '' }, + { label: 'Celebration', id: 'celebration' }, + { label: 'Shooting Star', id: 'shooting_star' }, + { label: 'Fireworks', id: 'fireworks' }, + { label: 'Lasers', id: 'lasers' }, + { label: 'Love', id: 'love' }, + { label: 'Confetti', id: 'confetti' }, + { label: 'Balloons', id: 'balloons' }, + { label: 'Spotlight', id: 'spotlight' }, + { label: 'Echo', id: 'echo' }, + { label: 'Invisible', id: 'invisible' }, + { label: 'Gentle', id: 'gentle' }, + { label: 'Loud', id: 'loud' }, + { label: 'Slam', id: 'slam' }, +] as const + +export const SendblueBlock: BlockConfig = { + type: 'sendblue', + name: 'Sendblue', + description: 'Send and receive iMessage and SMS', + longDescription: + 'Send iMessages and SMS to individuals or groups, check whether a number supports iMessage, show typing indicators, and look up message status with Sendblue. Trigger workflows on inbound messages and delivery status updates.', + docsLink: 'https://docs.sim.ai/tools/sendblue', + category: 'tools', + integrationType: IntegrationType.Communication, + tags: ['messaging', 'automation', 'webhooks'], + bgColor: '#008BFF', + icon: SendblueIcon, + authMode: AuthMode.ApiKey, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Send Message', id: 'sendblue_send_message' }, + { label: 'Send Group Message', id: 'sendblue_send_group_message' }, + { label: 'Evaluate Service', id: 'sendblue_evaluate_service' }, + { label: 'Send Typing Indicator', id: 'sendblue_send_typing_indicator' }, + { label: 'Get Message', id: 'sendblue_get_message' }, + ], + value: () => 'sendblue_send_message', + }, + { + id: 'apiKeyId', + title: 'API Key ID', + type: 'short-input', + placeholder: 'Your Sendblue API Key ID (sb-api-key-id)', + password: true, + required: true, + }, + { + id: 'apiSecretKey', + title: 'API Secret Key', + type: 'short-input', + placeholder: 'Your Sendblue API Secret Key (sb-api-secret-key)', + password: true, + required: true, + }, + { + id: 'from_number', + title: 'From Number', + type: 'short-input', + placeholder: 'e.g. +18887776666', + condition: { + field: 'operation', + value: [ + 'sendblue_send_message', + 'sendblue_send_group_message', + 'sendblue_send_typing_indicator', + ], + }, + required: { + field: 'operation', + value: ['sendblue_send_message', 'sendblue_send_group_message'], + }, + }, + { + id: 'number', + title: 'Recipient Number', + type: 'short-input', + placeholder: 'e.g. +19998887777', + condition: { + field: 'operation', + value: [ + 'sendblue_send_message', + 'sendblue_evaluate_service', + 'sendblue_send_typing_indicator', + ], + }, + required: { + field: 'operation', + value: [ + 'sendblue_send_message', + 'sendblue_evaluate_service', + 'sendblue_send_typing_indicator', + ], + }, + }, + { + id: 'numbers', + title: 'Recipient Numbers', + type: 'long-input', + placeholder: 'One phone number per line, e.g.\n+19998887777\n+13334445555', + condition: { field: 'operation', value: 'sendblue_send_group_message' }, + required: { field: 'operation', value: 'sendblue_send_group_message' }, + }, + { + id: 'content', + title: 'Message', + type: 'long-input', + placeholder: 'Message text (required unless a media URL is provided)', + condition: { + field: 'operation', + value: ['sendblue_send_message', 'sendblue_send_group_message'], + }, + }, + { + id: 'media_url', + title: 'Media URL', + type: 'short-input', + placeholder: 'https://example.com/image.jpg', + mode: 'advanced', + condition: { + field: 'operation', + value: ['sendblue_send_message', 'sendblue_send_group_message'], + }, + }, + { + id: 'send_style', + title: 'Send Style', + type: 'dropdown', + options: [...SEND_STYLE_OPTIONS], + value: () => '', + mode: 'advanced', + condition: { + field: 'operation', + value: ['sendblue_send_message', 'sendblue_send_group_message'], + }, + }, + { + id: 'group_id', + title: 'Group ID', + type: 'short-input', + placeholder: 'Existing group ID (leave blank to start a new group)', + mode: 'advanced', + condition: { field: 'operation', value: 'sendblue_send_group_message' }, + }, + { + id: 'status_callback', + title: 'Status Callback URL', + type: 'short-input', + placeholder: 'https://your-app.com/sendblue-status', + mode: 'advanced', + condition: { + field: 'operation', + value: ['sendblue_send_message', 'sendblue_send_group_message'], + }, + }, + { + id: 'message_id', + title: 'Message Handle / ID', + type: 'short-input', + placeholder: 'The message handle returned when sending', + condition: { field: 'operation', value: 'sendblue_get_message' }, + required: { field: 'operation', value: 'sendblue_get_message' }, + }, + ...getTrigger('sendblue_message_received').subBlocks, + ...getTrigger('sendblue_message_status_updated').subBlocks, + ], + + tools: { + access: [ + 'sendblue_send_message', + 'sendblue_send_group_message', + 'sendblue_evaluate_service', + 'sendblue_send_typing_indicator', + 'sendblue_get_message', + ], + config: { + tool: (params) => params.operation || 'sendblue_send_message', + params: (params) => { + const base: Record = { + apiKeyId: params.apiKeyId, + apiSecretKey: params.apiSecretKey, + } + + switch (params.operation) { + case 'sendblue_send_message': + return { + ...base, + number: params.number, + from_number: params.from_number, + content: params.content || undefined, + media_url: params.media_url || undefined, + send_style: params.send_style || undefined, + status_callback: params.status_callback || undefined, + } + case 'sendblue_send_group_message': + return { + ...base, + numbers: + typeof params.numbers === 'string' + ? params.numbers + .split('\n') + .map((n: string) => n.trim()) + .filter(Boolean) + : params.numbers, + from_number: params.from_number, + content: params.content || undefined, + media_url: params.media_url || undefined, + send_style: params.send_style || undefined, + group_id: params.group_id || undefined, + status_callback: params.status_callback || undefined, + } + case 'sendblue_evaluate_service': + return { ...base, number: params.number } + case 'sendblue_send_typing_indicator': + return { + ...base, + number: params.number, + from_number: params.from_number || undefined, + } + case 'sendblue_get_message': + return { ...base, message_id: params.message_id } + default: + return base + } + }, + }, + }, + + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKeyId: { type: 'string', description: 'Sendblue API Key ID' }, + apiSecretKey: { type: 'string', description: 'Sendblue API Secret Key' }, + number: { type: 'string', description: 'Recipient phone number (E.164)' }, + numbers: { type: 'string', description: 'Recipient phone numbers, one per line (E.164)' }, + from_number: { type: 'string', description: 'Sender Sendblue phone number (E.164)' }, + content: { type: 'string', description: 'Message text content' }, + media_url: { type: 'string', description: 'URL of media to send' }, + send_style: { type: 'string', description: 'iMessage expressive style' }, + group_id: { type: 'string', description: 'Existing group ID' }, + status_callback: { type: 'string', description: 'Status callback webhook URL' }, + message_id: { type: 'string', description: 'Message handle/ID to retrieve' }, + }, + + outputs: { + status: { type: 'string', description: 'Message or request status' }, + message_handle: { type: 'string', description: 'Unique message identifier' }, + service: { type: 'string', description: 'Service the number supports (iMessage or SMS)' }, + number: { type: 'string', description: 'Recipient phone number' }, + from_number: { type: 'string', description: 'Sending phone number' }, + content: { type: 'string', description: 'Message content' }, + media_url: { type: 'string', description: 'URL of attached media' }, + is_outbound: { type: 'boolean', description: 'Whether the message is outbound' }, + group_id: { type: 'string', description: 'Group identifier' }, + participants: { type: 'array', description: 'Group participant phone numbers' }, + send_style: { type: 'string', description: 'Expressive style applied' }, + account_email: { type: 'string', description: 'Account email' }, + sender_email: { type: 'string', description: 'Sending seat email' }, + seat_id: { type: 'string', description: 'Seat UUID' }, + status_code: { type: 'number', description: 'Numeric status code (typing indicator)' }, + error_code: { type: 'number', description: 'Numeric error code if failed' }, + error_message: { type: 'string', description: 'Error message if failed' }, + date_created: { type: 'string', description: 'Creation timestamp' }, + date_updated: { type: 'string', description: 'Last-update timestamp' }, + }, + + triggers: { + enabled: true, + available: ['sendblue_message_received', 'sendblue_message_status_updated'], + }, +} + +export const SendblueBlockMeta = { + tags: ['messaging', 'automation', 'webhooks'], + templates: [ + { + icon: SendblueIcon, + title: 'Sendblue lead speed-to-text', + prompt: + 'Build a workflow that fires when a new lead submits a form, drafts a friendly intro, and sends it as an iMessage via Sendblue within seconds so reps reach hot leads while they are still interested.', + modules: ['agent', 'workflows'], + category: 'sales', + tags: ['messaging', 'sales', 'automation'], + alsoIntegrations: ['typeform'], + }, + { + icon: SendblueIcon, + title: 'Sendblue appointment reminders', + prompt: + "Create a scheduled workflow that reads tomorrow's appointments from a table and sends each customer a personalized Sendblue iMessage reminder with the time and a reschedule link.", + modules: ['scheduled', 'tables', 'agent', 'workflows'], + category: 'operations', + tags: ['messaging', 'automation', 'scheduling'], + }, + { + icon: SendblueIcon, + title: 'Sendblue inbound reply autoresponder', + prompt: + 'Build a workflow triggered when a Sendblue message is received that classifies the inbound text, drafts a context-aware reply with an agent, and sends it back to the same number as an iMessage.', + modules: ['agent', 'workflows'], + category: 'support', + tags: ['messaging', 'automation', 'support'], + }, + { + icon: SendblueIcon, + title: 'Sendblue iMessage vs SMS routing', + prompt: + 'Create a workflow that evaluates whether a recipient number supports iMessage with Sendblue, then sends a rich iMessage when supported or a plain SMS fallback otherwise.', + modules: ['agent', 'workflows'], + category: 'engineering', + tags: ['messaging', 'automation'], + }, + { + icon: SendblueIcon, + title: 'Sendblue order-status notifier', + prompt: + 'Create a workflow triggered when an order ships that looks up the customer phone number and sends a Sendblue iMessage with the tracking number and estimated delivery date.', + modules: ['agent', 'workflows'], + category: 'operations', + tags: ['messaging', 'automation'], + alsoIntegrations: ['shopify'], + }, + { + icon: SendblueIcon, + title: 'Sendblue delivery-failure alerts', + prompt: + 'Build a workflow triggered by a Sendblue message status update that, when the status is ERROR, posts the failing number and error message to a Slack channel so the team can follow up.', + modules: ['agent', 'workflows'], + category: 'operations', + tags: ['messaging', 'automation', 'incident-management'], + alsoIntegrations: ['slack'], + }, + { + icon: SendblueIcon, + title: 'Sendblue group broadcast', + prompt: + 'Create a workflow that reads a list of VIP customers from a table and sends them a single Sendblue group iMessage announcing a new product launch with an image attachment.', + modules: ['tables', 'agent', 'workflows'], + category: 'marketing', + tags: ['messaging', 'marketing', 'automation'], + }, + { + icon: SendblueIcon, + title: 'Sendblue conversational support agent', + prompt: + 'Build a workflow triggered on inbound Sendblue messages that shows a typing indicator, looks up the customer in a table, answers their question with an agent, and replies over iMessage.', + modules: ['tables', 'agent', 'workflows'], + category: 'support', + tags: ['messaging', 'support', 'automation'], + }, + ], + skills: [ + { + name: 'send-imessage-notification', + description: 'Send an iMessage or SMS notification to a single recipient via Sendblue.', + content: + '# Send an iMessage or SMS Notification\n\nDeliver a message to one recipient through your Sendblue number.\n\n## Steps\n1. Choose the Send Message operation.\n2. Enter the Recipient Number and your From Number in E.164 format (for example +19998887777).\n3. Write the Message text. To send media instead of or alongside text, add a Media URL.\n4. Provide your Sendblue API Key ID and API Secret Key.\n\n## Output\nReturn the message status (QUEUED, SENT, DELIVERED, ERROR) and the message handle so the send can be tracked.', + }, + { + name: 'route-imessage-or-sms', + description: 'Check whether a number supports iMessage before sending with Sendblue.', + content: + '# Route iMessage vs SMS\n\nDecide how to reach a recipient based on their service.\n\n## Steps\n1. Use the Evaluate Service operation with the Recipient Number to learn whether the number supports iMessage or only SMS.\n2. Branch on the returned service value.\n3. Send a rich iMessage when supported, or a plain SMS fallback otherwise, using the Send Message operation.\n\n## Output\nReturn the evaluated number and its supported service so downstream steps can branch correctly.', + }, + { + name: 'reply-to-inbound-message', + description: 'Trigger on an inbound Sendblue message and reply automatically.', + content: + '# Reply to an Inbound Message\n\nRespond to customers as soon as they text in.\n\n## Steps\n1. Add the Sendblue Message Received trigger and point your Sendblue Receive Webhook at the generated URL.\n2. Read the inbound content, from_number, and service from the trigger output.\n3. Draft a reply with an agent and send it back with the Send Message operation, using the from_number as the recipient.\n\n## Output\nReturn the reply message handle and status so the conversation can be tracked.', + }, + ], +} as const satisfies BlockMeta diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index ea390a88cda..f9ba05533b9 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -242,6 +242,7 @@ import { SapS4HanaBlock, SapS4HanaBlockMeta } from '@/blocks/blocks/sap_s4hana' import { ScheduleBlock } from '@/blocks/blocks/schedule' import { SearchBlock } from '@/blocks/blocks/search' import { SecretsManagerBlock, SecretsManagerBlockMeta } from '@/blocks/blocks/secrets_manager' +import { SendblueBlock, SendblueBlockMeta } from '@/blocks/blocks/sendblue' import { SendGridBlock, SendGridBlockMeta } from '@/blocks/blocks/sendgrid' import { SentryBlock, SentryBlockMeta } from '@/blocks/blocks/sentry' import { SerperBlock, SerperBlockMeta } from '@/blocks/blocks/serper' @@ -525,6 +526,7 @@ const BLOCK_REGISTRY: Record = { schedule: ScheduleBlock, search: SearchBlock, secrets_manager: SecretsManagerBlock, + sendblue: SendblueBlock, sendgrid: SendGridBlock, sentry: SentryBlock, serper: SerperBlock, @@ -773,6 +775,7 @@ const BLOCK_META_REGISTRY: Record = { sap_concur: SapConcurBlockMeta, sap_s4hana: SapS4HanaBlockMeta, secrets_manager: SecretsManagerBlockMeta, + sendblue: SendblueBlockMeta, sendgrid: SendGridBlockMeta, sentry: SentryBlockMeta, serper: SerperBlockMeta, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 231fe39bcc3..fc806f78bcd 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2047,6 +2047,21 @@ export function TwilioIcon(props: SVGProps) { ) } +export function SendblueIcon(props: SVGProps) { + return ( + + + + + ) +} + export function ImageIcon(props: SVGProps) { return ( = { sap_concur: SapConcurIcon, sap_s4hana: SapS4HanaIcon, secrets_manager: SecretsManagerIcon, + sendblue: SendblueIcon, sendgrid: SendgridIcon, sentry: SentryIcon, serper: SerperIcon, diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index b6ca393af8b..2d901910a2b 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -12699,6 +12699,56 @@ "integrationType": "hr", "tags": ["automation"] }, + { + "type": "sendblue", + "slug": "sendblue", + "name": "Sendblue", + "description": "Send and receive iMessage and SMS", + "longDescription": "Send iMessages and SMS to individuals or groups, check whether a number supports iMessage, show typing indicators, and look up message status with Sendblue. Trigger workflows on inbound messages and delivery status updates.", + "bgColor": "#008BFF", + "iconName": "SendblueIcon", + "docsUrl": "https://docs.sim.ai/tools/sendblue", + "operations": [ + { + "name": "Send Message", + "description": "Send an iMessage or SMS to a single recipient via Sendblue." + }, + { + "name": "Send Group Message", + "description": "Send an iMessage or SMS to a group of recipients via Sendblue." + }, + { + "name": "Evaluate Service", + "description": "Check whether a phone number can receive iMessage or only SMS." + }, + { + "name": "Send Typing Indicator", + "description": "Display a typing indicator to a recipient (not supported in group chats)." + }, + { + "name": "Get Message", + "description": "Retrieve a single message and its current status by message handle/ID." + } + ], + "operationCount": 5, + "triggers": [ + { + "id": "sendblue_message_received", + "name": "Sendblue Message Received", + "description": "Trigger when an inbound iMessage or SMS is received in Sendblue" + }, + { + "id": "sendblue_message_status_updated", + "name": "Sendblue Message Status Updated", + "description": "Trigger when an outbound message status changes (SENT, DELIVERED, ERROR) in Sendblue" + } + ], + "triggerCount": 2, + "authType": "none", + "category": "tools", + "integrationType": "communication", + "tags": ["messaging", "automation", "webhooks"] + }, { "type": "sendgrid", "slug": "sendgrid", diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 44b3a0b5009..487cc851727 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -32,6 +32,7 @@ import { outlookHandler } from '@/lib/webhooks/providers/outlook' import { resendHandler } from '@/lib/webhooks/providers/resend' import { rssHandler } from '@/lib/webhooks/providers/rss' import { salesforceHandler } from '@/lib/webhooks/providers/salesforce' +import { sendblueHandler } from '@/lib/webhooks/providers/sendblue' import { servicenowHandler } from '@/lib/webhooks/providers/servicenow' import { slackHandler } from '@/lib/webhooks/providers/slack' import { stripeHandler } from '@/lib/webhooks/providers/stripe' @@ -82,6 +83,7 @@ const PROVIDER_HANDLERS: Record = { outlook: outlookHandler, rss: rssHandler, salesforce: salesforceHandler, + sendblue: sendblueHandler, servicenow: servicenowHandler, slack: slackHandler, stripe: stripeHandler, diff --git a/apps/sim/lib/webhooks/providers/sendblue.ts b/apps/sim/lib/webhooks/providers/sendblue.ts new file mode 100644 index 00000000000..da8ba930175 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/sendblue.ts @@ -0,0 +1,88 @@ +import { createLogger } from '@sim/logger' +import { getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import type { + EventMatchContext, + FormatInputContext, + FormatInputResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' + +const logger = createLogger('WebhookProvider:Sendblue') + +/** + * Maps Sendblue trigger IDs to the expected value of the payload `is_outbound` + * flag. Inbound messages are routed to the "message received" trigger and + * outbound status callbacks to the "message status updated" trigger. + */ +const TRIGGER_IS_OUTBOUND: Record = { + sendblue_message_received: false, + sendblue_message_status_updated: true, +} + +export const sendblueHandler: WebhookProviderHandler = { + matchEvent({ body, webhook, requestId }: EventMatchContext): boolean { + const providerConfig = getProviderConfig(webhook) + const triggerId = providerConfig.triggerId as string | undefined + if (!triggerId || !(triggerId in TRIGGER_IS_OUTBOUND)) return true + + if (!isRecord(body)) { + logger.warn(`[${requestId}] Sendblue webhook payload was not an object`) + return false + } + + const expected = TRIGGER_IS_OUTBOUND[triggerId] + const isOutbound = body.is_outbound === true + if (isOutbound !== expected) { + logger.info(`[${requestId}] Sendblue event did not match trigger`, { triggerId, isOutbound }) + return false + } + + return true + }, + + extractIdempotencyId(body: unknown): string | null { + if (!isRecord(body)) return null + const handle = body.message_handle + return typeof handle === 'string' && handle.length > 0 ? handle : null + }, + + async formatInput({ body }: FormatInputContext): Promise { + const b = isRecord(body) ? body : {} + return { + input: { + accountEmail: b.accountEmail ?? b.account_email ?? null, + content: b.content ?? null, + media_url: b.media_url ?? null, + is_outbound: b.is_outbound ?? null, + status: b.status ?? null, + error_code: b.error_code ?? null, + error_message: b.error_message ?? null, + error_reason: b.error_reason ?? null, + error_detail: b.error_detail ?? null, + message_handle: b.message_handle ?? null, + date_sent: b.date_sent ?? null, + date_updated: b.date_updated ?? null, + from_number: b.from_number ?? null, + number: b.number ?? null, + to_number: b.to_number ?? null, + was_downgraded: b.was_downgraded ?? null, + plan: b.plan ?? null, + message_type: b.message_type ?? null, + group_id: b.group_id ?? null, + participants: b.participants ?? [], + send_style: b.send_style ?? null, + opted_out: b.opted_out ?? null, + sendblue_number: b.sendblue_number ?? null, + service: b.service ?? null, + group_display_name: b.group_display_name ?? null, + sender_email: b.sender_email ?? null, + seat_id: b.seat_id ?? null, + raw: JSON.stringify(b), + }, + } + }, +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index a40f1ac7335..35eb640eaed 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -2637,6 +2637,13 @@ import { secretsManagerListSecretsTool, secretsManagerUpdateSecretTool, } from '@/tools/secrets_manager' +import { + sendblueEvaluateServiceTool, + sendblueGetMessageTool, + sendblueSendGroupMessageTool, + sendblueSendMessageTool, + sendblueSendTypingIndicatorTool, +} from '@/tools/sendblue' import { sendGridAddContactsToListTool, sendGridAddContactTool, @@ -3659,6 +3666,11 @@ export const tools: Record = { resend_update_contact: resendUpdateContactTool, resend_delete_contact: resendDeleteContactTool, resend_list_domains: resendListDomainsTool, + sendblue_send_message: sendblueSendMessageTool, + sendblue_send_group_message: sendblueSendGroupMessageTool, + sendblue_evaluate_service: sendblueEvaluateServiceTool, + sendblue_send_typing_indicator: sendblueSendTypingIndicatorTool, + sendblue_get_message: sendblueGetMessageTool, sendgrid_send_mail: sendGridSendMailTool, sendgrid_add_contact: sendGridAddContactTool, sendgrid_get_contact: sendGridGetContactTool, diff --git a/apps/sim/tools/sendblue/evaluate_service.ts b/apps/sim/tools/sendblue/evaluate_service.ts new file mode 100644 index 00000000000..0632f52d799 --- /dev/null +++ b/apps/sim/tools/sendblue/evaluate_service.ts @@ -0,0 +1,56 @@ +import type { + SendblueEvaluateServiceParams, + SendblueEvaluateServiceResponse, +} from '@/tools/sendblue/types' +import { + SENDBLUE_API_BASE_URL, + sendblueBaseParamFields, + sendblueHeaders, +} from '@/tools/sendblue/utils' +import type { ToolConfig } from '@/tools/types' + +export const sendblueEvaluateServiceTool: ToolConfig< + SendblueEvaluateServiceParams, + SendblueEvaluateServiceResponse +> = { + id: 'sendblue_evaluate_service', + name: 'Sendblue Evaluate Service', + description: 'Check whether a phone number can receive iMessage or only SMS.', + version: '1.0.0', + + params: { + ...sendblueBaseParamFields, + number: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Phone number to evaluate, in E.164 format (e.g., +19998887777)', + }, + }, + + request: { + url: (params) => { + const url = new URL(`${SENDBLUE_API_BASE_URL}/api/evaluate-service`) + url.searchParams.set('number', params.number) + return url.toString() + }, + method: 'GET', + headers: (params) => sendblueHeaders(params), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + number: data.number ?? null, + service: data.service ?? null, + }, + } + }, + + outputs: { + number: { type: 'string', description: 'The evaluated phone number in E.164 format' }, + service: { type: 'string', description: 'The service the number supports: iMessage or SMS' }, + }, +} diff --git a/apps/sim/tools/sendblue/get_message.ts b/apps/sim/tools/sendblue/get_message.ts new file mode 100644 index 00000000000..53713f4cf91 --- /dev/null +++ b/apps/sim/tools/sendblue/get_message.ts @@ -0,0 +1,130 @@ +import type { SendblueGetMessageParams, SendblueGetMessageResponse } from '@/tools/sendblue/types' +import { + SENDBLUE_API_BASE_URL, + sendblueBaseParamFields, + sendblueHeaders, +} from '@/tools/sendblue/utils' +import type { ToolConfig } from '@/tools/types' + +export const sendblueGetMessageTool: ToolConfig< + SendblueGetMessageParams, + SendblueGetMessageResponse +> = { + id: 'sendblue_get_message', + name: 'Sendblue Get Message', + description: 'Retrieve a single message and its current status by message handle/ID.', + version: '1.0.0', + + params: { + ...sendblueBaseParamFields, + message_id: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The message handle/ID returned when the message was sent.', + }, + }, + + request: { + url: (params) => + `${SENDBLUE_API_BASE_URL}/api/v2/messages/${encodeURIComponent(params.message_id)}`, + method: 'GET', + headers: (params) => sendblueHeaders(params), + }, + + transformResponse: async (response) => { + const body = await response.json() + const data = body?.data ?? body ?? {} + return { + success: true, + output: { + status: data.status ?? null, + message_handle: data.message_handle ?? null, + account_email: data.accountEmail ?? data.account_email ?? null, + content: data.content ?? null, + is_outbound: data.is_outbound ?? null, + from_number: data.from_number ?? null, + number: data.number ?? null, + to_number: data.to_number ?? null, + media_url: data.media_url ?? null, + message_type: data.message_type ?? null, + service: data.service ?? null, + group_id: data.group_id ?? null, + group_display_name: data.group_display_name ?? null, + participants: data.participants ?? [], + send_style: data.send_style ?? null, + was_downgraded: data.was_downgraded ?? null, + opted_out: data.opted_out ?? null, + plan: data.plan ?? null, + sendblue_number: data.sendblue_number ?? null, + seat_id: data.seat_id ?? null, + sender_email: data.sender_email ?? null, + error_code: data.error_code ?? null, + error_message: data.error_message ?? null, + error_reason: data.error_reason ?? null, + error_detail: data.error_detail ?? null, + date_sent: data.date_sent ?? null, + date_updated: data.date_updated ?? null, + }, + } + }, + + outputs: { + status: { type: 'string', description: 'Current message status' }, + message_handle: { type: 'string', description: 'Unique message identifier' }, + account_email: { type: 'string', description: 'Email of the account', optional: true }, + content: { type: 'string', description: 'Message content', optional: true }, + is_outbound: { + type: 'boolean', + description: 'Whether the message is outbound', + optional: true, + }, + from_number: { type: 'string', description: 'Sending phone number', optional: true }, + number: { type: 'string', description: 'Recipient phone number', optional: true }, + to_number: { type: 'string', description: 'Destination phone number', optional: true }, + media_url: { type: 'string', description: 'URL of attached media', optional: true }, + message_type: { + type: 'string', + description: 'Message category: message or group', + optional: true, + }, + service: { + type: 'string', + description: 'Messaging service: iMessage, SMS, or RCS', + optional: true, + }, + group_id: { + type: 'string', + description: 'Group identifier (empty for non-group)', + optional: true, + }, + group_display_name: { type: 'string', description: 'Group chat name', optional: true }, + participants: { + type: 'array', + description: 'Participant phone numbers', + items: { type: 'string' }, + optional: true, + }, + send_style: { type: 'string', description: 'Expressive style applied', optional: true }, + was_downgraded: { + type: 'boolean', + description: 'True if the recipient lacks iMessage support', + optional: true, + }, + opted_out: { + type: 'boolean', + description: 'True if the recipient has opted out', + optional: true, + }, + plan: { type: 'string', description: 'Account plan type', optional: true }, + sendblue_number: { type: 'string', description: 'Sendblue phone number used', optional: true }, + seat_id: { type: 'string', description: 'Seat UUID', optional: true }, + sender_email: { type: 'string', description: 'Email of the sending seat', optional: true }, + error_code: { type: 'number', description: 'Numeric error code if failed', optional: true }, + error_message: { type: 'string', description: 'Error message if failed', optional: true }, + error_reason: { type: 'string', description: 'Additional error context', optional: true }, + error_detail: { type: 'string', description: 'Detailed error information', optional: true }, + date_sent: { type: 'string', description: 'ISO 8601 creation timestamp', optional: true }, + date_updated: { type: 'string', description: 'ISO 8601 last-update timestamp', optional: true }, + }, +} diff --git a/apps/sim/tools/sendblue/index.ts b/apps/sim/tools/sendblue/index.ts new file mode 100644 index 00000000000..1fd0b54e519 --- /dev/null +++ b/apps/sim/tools/sendblue/index.ts @@ -0,0 +1,6 @@ +export { sendblueEvaluateServiceTool } from '@/tools/sendblue/evaluate_service' +export { sendblueGetMessageTool } from '@/tools/sendblue/get_message' +export { sendblueSendGroupMessageTool } from '@/tools/sendblue/send_group_message' +export { sendblueSendMessageTool } from '@/tools/sendblue/send_message' +export { sendblueSendTypingIndicatorTool } from '@/tools/sendblue/send_typing_indicator' +export * from '@/tools/sendblue/types' diff --git a/apps/sim/tools/sendblue/send_group_message.ts b/apps/sim/tools/sendblue/send_group_message.ts new file mode 100644 index 00000000000..ebc5582af93 --- /dev/null +++ b/apps/sim/tools/sendblue/send_group_message.ts @@ -0,0 +1,165 @@ +import { filterUndefined } from '@sim/utils/object' +import type { + SendblueSendGroupMessageParams, + SendblueSendGroupMessageResponse, +} from '@/tools/sendblue/types' +import { + SENDBLUE_API_BASE_URL, + sendblueBaseParamFields, + sendblueHeaders, +} from '@/tools/sendblue/utils' +import type { ToolConfig } from '@/tools/types' + +export const sendblueSendGroupMessageTool: ToolConfig< + SendblueSendGroupMessageParams, + SendblueSendGroupMessageResponse +> = { + id: 'sendblue_send_group_message', + name: 'Sendblue Send Group Message', + description: 'Send an iMessage or SMS to a group of recipients via Sendblue.', + version: '1.0.0', + + params: { + ...sendblueBaseParamFields, + numbers: { + type: 'array', + required: true, + visibility: 'user-or-llm', + description: + 'Recipient phone numbers in E.164 format (e.g., ["+19998887777", "+13334445555"])', + items: { type: 'string', description: 'Phone number in E.164 format' }, + }, + from_number: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'One of your registered Sendblue phone numbers to send from, in E.164 format (e.g., +18887776666)', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Message text content. Either content or media_url must be provided.', + }, + media_url: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL of a media file to send. Either content or media_url must be provided.', + }, + send_style: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'iMessage expressive style (e.g., celebration, fireworks, lasers, confetti, balloons, invisible, slam).', + }, + group_id: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Unique identifier of an existing group to send to. Omit to start a new group.', + }, + status_callback: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Webhook URL that Sendblue will POST message status updates to.', + }, + }, + + request: { + url: `${SENDBLUE_API_BASE_URL}/api/send-group-message`, + method: 'POST', + headers: (params) => sendblueHeaders(params), + body: (params) => + filterUndefined({ + numbers: params.numbers, + from_number: params.from_number, + content: params.content, + media_url: params.media_url, + send_style: params.send_style, + group_id: params.group_id, + status_callback: params.status_callback, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + status: data.status ?? null, + message_handle: data.message_handle ?? null, + account_email: data.account_email ?? data.accountEmail ?? null, + content: data.content ?? null, + is_outbound: data.is_outbound ?? null, + from_number: data.from_number ?? null, + number: data.number ?? null, + media_url: data.media_url ?? null, + send_style: data.send_style ?? null, + seat_id: data.seat_id ?? null, + sender_email: data.sender_email ?? null, + error_code: data.error_code ?? null, + error_message: data.error_message ?? null, + date_created: data.date_created ?? null, + date_updated: data.date_updated ?? null, + group_id: data.group_id ?? null, + participants: data.participants ?? [], + }, + } + }, + + outputs: { + status: { type: 'string', description: 'Message status: QUEUED, SENT, DELIVERED, or ERROR' }, + message_handle: { type: 'string', description: 'Unique identifier for tracking the message' }, + group_id: { + type: 'string', + description: 'Identifier of the group the message was sent to', + optional: true, + }, + participants: { + type: 'array', + description: 'Phone numbers participating in the group', + items: { type: 'string' }, + }, + account_email: { type: 'string', description: 'Email of the account that sent the message' }, + content: { type: 'string', description: 'Message content', optional: true }, + is_outbound: { type: 'boolean', description: 'Whether this is an outbound message' }, + from_number: { type: 'string', description: 'Sending phone number' }, + number: { type: 'string', description: 'Recipient phone number', optional: true }, + media_url: { type: 'string', description: 'URL of attached media', optional: true }, + send_style: { + type: 'string', + description: 'iMessage expressive style applied', + optional: true, + }, + seat_id: { + type: 'string', + description: 'UUID of the seat that sent the message', + optional: true, + }, + sender_email: { + type: 'string', + description: 'Email of the seat (user) that sent the message', + optional: true, + }, + error_code: { + type: 'number', + description: 'Numeric error code if the message failed', + optional: true, + }, + error_message: { + type: 'string', + description: 'Error message if the message failed', + optional: true, + }, + date_created: { type: 'string', description: 'When the message was created', optional: true }, + date_updated: { + type: 'string', + description: 'When the message was last updated', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/sendblue/send_message.ts b/apps/sim/tools/sendblue/send_message.ts new file mode 100644 index 00000000000..325382da019 --- /dev/null +++ b/apps/sim/tools/sendblue/send_message.ts @@ -0,0 +1,141 @@ +import { filterUndefined } from '@sim/utils/object' +import type { SendblueSendMessageParams, SendblueSendMessageResponse } from '@/tools/sendblue/types' +import { + SENDBLUE_API_BASE_URL, + sendblueBaseParamFields, + sendblueHeaders, +} from '@/tools/sendblue/utils' +import type { ToolConfig } from '@/tools/types' + +export const sendblueSendMessageTool: ToolConfig< + SendblueSendMessageParams, + SendblueSendMessageResponse +> = { + id: 'sendblue_send_message', + name: 'Sendblue Send Message', + description: 'Send an iMessage or SMS to a single recipient via Sendblue.', + version: '1.0.0', + + params: { + ...sendblueBaseParamFields, + number: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Recipient phone number in E.164 format (e.g., +19998887777)', + }, + from_number: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'One of your registered Sendblue phone numbers to send from, in E.164 format (e.g., +18887776666)', + }, + content: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Message text content. Either content or media_url must be provided.', + }, + media_url: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'URL of a media file to send. Either content or media_url must be provided.', + }, + send_style: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'iMessage expressive style (e.g., celebration, fireworks, lasers, confetti, balloons, invisible, slam).', + }, + status_callback: { + type: 'string', + required: false, + visibility: 'user-only', + description: 'Webhook URL that Sendblue will POST message status updates to.', + }, + }, + + request: { + url: `${SENDBLUE_API_BASE_URL}/api/send-message`, + method: 'POST', + headers: (params) => sendblueHeaders(params), + body: (params) => + filterUndefined({ + number: params.number, + from_number: params.from_number, + content: params.content, + media_url: params.media_url, + send_style: params.send_style, + status_callback: params.status_callback, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + status: data.status ?? null, + message_handle: data.message_handle ?? null, + account_email: data.account_email ?? data.accountEmail ?? null, + content: data.content ?? null, + is_outbound: data.is_outbound ?? null, + from_number: data.from_number ?? null, + number: data.number ?? null, + media_url: data.media_url ?? null, + send_style: data.send_style ?? null, + seat_id: data.seat_id ?? null, + sender_email: data.sender_email ?? null, + error_code: data.error_code ?? null, + error_message: data.error_message ?? null, + date_created: data.date_created ?? null, + date_updated: data.date_updated ?? null, + }, + } + }, + + outputs: { + status: { type: 'string', description: 'Message status: QUEUED, SENT, DELIVERED, or ERROR' }, + message_handle: { type: 'string', description: 'Unique identifier for tracking the message' }, + account_email: { type: 'string', description: 'Email of the account that sent the message' }, + content: { type: 'string', description: 'Message content', optional: true }, + is_outbound: { type: 'boolean', description: 'Whether this is an outbound message' }, + from_number: { type: 'string', description: 'Sending phone number' }, + number: { type: 'string', description: 'Recipient phone number' }, + media_url: { type: 'string', description: 'URL of attached media', optional: true }, + send_style: { + type: 'string', + description: 'iMessage expressive style applied', + optional: true, + }, + seat_id: { + type: 'string', + description: 'UUID of the seat that sent the message', + optional: true, + }, + sender_email: { + type: 'string', + description: 'Email of the seat (user) that sent the message', + optional: true, + }, + error_code: { + type: 'number', + description: 'Numeric error code if the message failed', + optional: true, + }, + error_message: { + type: 'string', + description: 'Error message if the message failed', + optional: true, + }, + date_created: { type: 'string', description: 'When the message was created', optional: true }, + date_updated: { + type: 'string', + description: 'When the message was last updated', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/sendblue/send_typing_indicator.ts b/apps/sim/tools/sendblue/send_typing_indicator.ts new file mode 100644 index 00000000000..7200c2f0151 --- /dev/null +++ b/apps/sim/tools/sendblue/send_typing_indicator.ts @@ -0,0 +1,75 @@ +import { filterUndefined } from '@sim/utils/object' +import type { + SendblueTypingIndicatorParams, + SendblueTypingIndicatorResponse, +} from '@/tools/sendblue/types' +import { + SENDBLUE_API_BASE_URL, + sendblueBaseParamFields, + sendblueHeaders, +} from '@/tools/sendblue/utils' +import type { ToolConfig } from '@/tools/types' + +export const sendblueSendTypingIndicatorTool: ToolConfig< + SendblueTypingIndicatorParams, + SendblueTypingIndicatorResponse +> = { + id: 'sendblue_send_typing_indicator', + name: 'Sendblue Send Typing Indicator', + description: 'Display a typing indicator to a recipient (not supported in group chats).', + version: '1.0.0', + + params: { + ...sendblueBaseParamFields, + number: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: "Recipient's phone number in E.164 format (e.g., +19998887777)", + }, + from_number: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Your Sendblue line number to send from, in E.164 format.', + }, + }, + + request: { + url: `${SENDBLUE_API_BASE_URL}/api/send-typing-indicator`, + method: 'POST', + headers: (params) => sendblueHeaders(params), + body: (params) => + filterUndefined({ + number: params.number, + from_number: params.from_number, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + status: data.status ?? null, + status_code: data.status_code ?? null, + number: data.number ?? null, + error_message: data.error_message ?? null, + }, + } + }, + + outputs: { + status: { + type: 'string', + description: 'Delivery status of the typing indicator (e.g., QUEUED)', + }, + status_code: { type: 'number', description: 'Numeric status code returned by Sendblue' }, + number: { type: 'string', description: 'The recipient phone number' }, + error_message: { + type: 'string', + description: 'Error details, null on success', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/sendblue/types.ts b/apps/sim/tools/sendblue/types.ts new file mode 100644 index 00000000000..2abcdf9ef82 --- /dev/null +++ b/apps/sim/tools/sendblue/types.ts @@ -0,0 +1,136 @@ +import type { ToolResponse } from '@/tools/types' + +/** + * iMessage expressive styles supported by Sendblue. + */ +export type SendblueSendStyle = + | 'celebration' + | 'shooting_star' + | 'fireworks' + | 'lasers' + | 'love' + | 'confetti' + | 'balloons' + | 'spotlight' + | 'echo' + | 'invisible' + | 'gentle' + | 'loud' + | 'slam' + +export interface SendblueBaseParams { + apiKeyId: string + apiSecretKey: string +} + +export interface SendblueSendMessageParams extends SendblueBaseParams { + number: string + from_number: string + content?: string + media_url?: string + send_style?: string + status_callback?: string +} + +export interface SendblueSendGroupMessageParams extends SendblueBaseParams { + numbers: string[] + from_number: string + content?: string + media_url?: string + send_style?: string + group_id?: string + status_callback?: string +} + +export interface SendblueEvaluateServiceParams extends SendblueBaseParams { + number: string +} + +export interface SendblueTypingIndicatorParams extends SendblueBaseParams { + number: string + from_number?: string +} + +export interface SendblueGetMessageParams extends SendblueBaseParams { + message_id: string +} + +/** + * Shared shape of a Sendblue message resource returned by the send endpoints. + */ +export interface SendblueMessageOutput { + status: string | null + message_handle: string | null + account_email: string | null + content: string | null + is_outbound: boolean | null + from_number: string | null + number: string | null + media_url: string | null + send_style: string | null + seat_id: string | null + sender_email: string | null + error_code: number | null + error_message: string | null + date_created: string | null + date_updated: string | null +} + +export interface SendblueSendMessageResponse extends ToolResponse { + output: SendblueMessageOutput +} + +export interface SendblueSendGroupMessageResponse extends ToolResponse { + output: SendblueMessageOutput & { + group_id: string | null + participants: string[] + } +} + +export interface SendblueEvaluateServiceResponse extends ToolResponse { + output: { + number: string | null + service: string | null + } +} + +export interface SendblueTypingIndicatorResponse extends ToolResponse { + output: { + status: string | null + status_code: number | null + number: string | null + error_message: string | null + } +} + +export interface SendblueGetMessageResponse extends ToolResponse { + output: { + status: string | null + message_handle: string | null + account_email: string | null + content: string | null + is_outbound: boolean | null + from_number: string | null + number: string | null + to_number: string | null + media_url: string | null + message_type: string | null + service: string | null + group_id: string | null + group_display_name: string | null + participants: string[] + send_style: string | null + was_downgraded: boolean | null + opted_out: boolean | null + plan: string | null + sendblue_number: string | null + seat_id: string | null + sender_email: string | null + error_code: number | null + error_message: string | null + error_reason: string | null + error_detail: string | null + date_sent: string | null + date_updated: string | null + } +} diff --git a/apps/sim/tools/sendblue/utils.ts b/apps/sim/tools/sendblue/utils.ts new file mode 100644 index 00000000000..f87f9696e12 --- /dev/null +++ b/apps/sim/tools/sendblue/utils.ts @@ -0,0 +1,38 @@ +import type { SendblueBaseParams } from '@/tools/sendblue/types' +import type { ToolConfig } from '@/tools/types' + +/** + * Base URL for the Sendblue API as documented at https://docs.sendblue.com/api-v2/. + */ +export const SENDBLUE_API_BASE_URL = 'https://api.sendblue.com' + +/** + * Builds the authentication headers required by every Sendblue endpoint. + * Sendblue authenticates with an API Key ID and API Secret Key sent as + * the `sb-api-key-id` and `sb-api-secret-key` headers. + */ +export function sendblueHeaders(params: SendblueBaseParams): Record { + return { + 'sb-api-key-id': params.apiKeyId.trim(), + 'sb-api-secret-key': params.apiSecretKey.trim(), + 'Content-Type': 'application/json', + } +} + +/** + * Shared credential param definitions reused across all Sendblue tools. + */ +export const sendblueBaseParamFields = { + apiKeyId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sendblue API Key ID (sb-api-key-id)', + }, + apiSecretKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Sendblue API Secret Key (sb-api-secret-key)', + }, +} satisfies ToolConfig['params'] diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 0de5587cb5b..d6c0c40b25f 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -290,6 +290,10 @@ import { salesforceRecordUpdatedTrigger, salesforceWebhookTrigger, } from '@/triggers/salesforce' +import { + sendblueMessageReceivedTrigger, + sendblueMessageStatusUpdatedTrigger, +} from '@/triggers/sendblue' import { servicenowChangeRequestCreatedTrigger, servicenowChangeRequestUpdatedTrigger, @@ -553,6 +557,8 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { salesforce_opportunity_stage_changed: salesforceOpportunityStageChangedTrigger, salesforce_case_status_changed: salesforceCaseStatusChangedTrigger, salesforce_webhook: salesforceWebhookTrigger, + sendblue_message_received: sendblueMessageReceivedTrigger, + sendblue_message_status_updated: sendblueMessageStatusUpdatedTrigger, servicenow_incident_created: servicenowIncidentCreatedTrigger, servicenow_incident_updated: servicenowIncidentUpdatedTrigger, servicenow_change_request_created: servicenowChangeRequestCreatedTrigger, diff --git a/apps/sim/triggers/sendblue/index.ts b/apps/sim/triggers/sendblue/index.ts new file mode 100644 index 00000000000..6d5b9fac14d --- /dev/null +++ b/apps/sim/triggers/sendblue/index.ts @@ -0,0 +1,2 @@ +export { sendblueMessageReceivedTrigger } from '@/triggers/sendblue/message_received' +export { sendblueMessageStatusUpdatedTrigger } from '@/triggers/sendblue/message_status_updated' diff --git a/apps/sim/triggers/sendblue/message_received.ts b/apps/sim/triggers/sendblue/message_received.ts new file mode 100644 index 00000000000..a72a461333c --- /dev/null +++ b/apps/sim/triggers/sendblue/message_received.ts @@ -0,0 +1,25 @@ +import { SendblueIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildSendblueOutputs, + sendblueSetupInstructions, + sendblueTriggerOptions, +} from '@/triggers/sendblue/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const sendblueMessageReceivedTrigger: TriggerConfig = { + id: 'sendblue_message_received', + name: 'Sendblue Message Received', + provider: 'sendblue', + description: 'Trigger when an inbound iMessage or SMS is received in Sendblue', + version: '1.0.0', + icon: SendblueIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'sendblue_message_received', + triggerOptions: sendblueTriggerOptions, + includeDropdown: true, + setupInstructions: sendblueSetupInstructions('Message Received'), + }), + outputs: buildSendblueOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/sendblue/message_status_updated.ts b/apps/sim/triggers/sendblue/message_status_updated.ts new file mode 100644 index 00000000000..1c5e924fb89 --- /dev/null +++ b/apps/sim/triggers/sendblue/message_status_updated.ts @@ -0,0 +1,25 @@ +import { SendblueIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + buildSendblueOutputs, + sendblueSetupInstructions, + sendblueTriggerOptions, +} from '@/triggers/sendblue/utils' +import type { TriggerConfig } from '@/triggers/types' + +export const sendblueMessageStatusUpdatedTrigger: TriggerConfig = { + id: 'sendblue_message_status_updated', + name: 'Sendblue Message Status Updated', + provider: 'sendblue', + description: + 'Trigger when an outbound message status changes (SENT, DELIVERED, ERROR) in Sendblue', + version: '1.0.0', + icon: SendblueIcon, + subBlocks: buildTriggerSubBlocks({ + triggerId: 'sendblue_message_status_updated', + triggerOptions: sendblueTriggerOptions, + setupInstructions: sendblueSetupInstructions('Message Status Updated'), + }), + outputs: buildSendblueOutputs(), + webhook: { method: 'POST', headers: { 'Content-Type': 'application/json' } }, +} diff --git a/apps/sim/triggers/sendblue/utils.ts b/apps/sim/triggers/sendblue/utils.ts new file mode 100644 index 00000000000..2bae0a72960 --- /dev/null +++ b/apps/sim/triggers/sendblue/utils.ts @@ -0,0 +1,85 @@ +import type { TriggerOutput } from '@/triggers/types' + +/** + * Maps Sendblue trigger IDs to the expected value of the webhook payload's + * `is_outbound` flag, used to route inbound vs. outbound status events. + */ +export const SENDBLUE_TRIGGER_IS_OUTBOUND: Record = { + sendblue_message_received: false, + sendblue_message_status_updated: true, +} + +export const sendblueTriggerOptions = [ + { label: 'Message Received', id: 'sendblue_message_received' }, + { label: 'Message Status Updated', id: 'sendblue_message_status_updated' }, +] + +export function sendblueSetupInstructions(eventType: string): string { + const instructions = [ + 'Copy the Webhook URL above.', + 'Open your Sendblue dashboard and go to Settings > Webhooks.', + eventType === 'Message Received' + ? 'Paste the Webhook URL into the Receive Webhook field to receive inbound messages.' + : 'Paste the Webhook URL into the Send / Status Webhook field to receive outbound message status updates. You can also pass it per-message as the status_callback parameter.', + 'Save your webhook settings in Sendblue.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Outputs shared by both Sendblue message webhooks. Inbound and outbound + * status callbacks use the same payload schema. + */ +export function buildSendblueOutputs(): Record { + return { + accountEmail: { type: 'string', description: 'Email of the Sendblue account' }, + content: { type: 'string', description: 'Message text content' }, + media_url: { type: 'string', description: 'CDN link to attached media, if any' }, + is_outbound: { type: 'boolean', description: 'True for outbound messages, false for inbound' }, + status: { + type: 'string', + description: 'Message status (e.g., RECEIVED, QUEUED, SENT, DELIVERED, ERROR)', + }, + error_code: { type: 'number', description: 'Error identifier, null if none' }, + error_message: { type: 'string', description: 'Descriptive error text, null if none' }, + error_reason: { type: 'string', description: 'Additional error context, null if none' }, + error_detail: { type: 'string', description: 'Detailed error information, null if none' }, + message_handle: { + type: 'string', + description: 'Sendblue message identifier (use to deduplicate)', + }, + date_sent: { type: 'string', description: 'ISO 8601 creation timestamp' }, + date_updated: { type: 'string', description: 'ISO 8601 last-update timestamp' }, + from_number: { type: 'string', description: 'E.164 sender phone number' }, + number: { type: 'string', description: 'E.164 recipient/counterparty phone number' }, + to_number: { type: 'string', description: 'E.164 destination phone number' }, + was_downgraded: { + type: 'boolean', + description: 'True if the recipient lacks iMessage support', + }, + plan: { type: 'string', description: 'Account plan type' }, + message_type: { type: 'string', description: 'Message category (e.g., message, group)' }, + group_id: { type: 'string', description: 'Group identifier, empty for non-group messages' }, + participants: { type: 'array', description: 'Participant phone numbers for group messages' }, + send_style: { type: 'string', description: 'Expressive style if applied' }, + opted_out: { type: 'boolean', description: 'True if the recipient has opted out' }, + sendblue_number: { type: 'string', description: 'Sendblue phone number used' }, + service: { type: 'string', description: 'Messaging service (iMessage or SMS)' }, + group_display_name: { + type: 'string', + description: 'Group chat name, null for non-group messages', + }, + sender_email: { type: 'string', description: 'Email of the user who sent the message' }, + seat_id: { type: 'string', description: 'Seat UUID, null if absent' }, + raw: { + type: 'string', + description: 'Complete raw webhook payload from Sendblue as a JSON string', + }, + } +} From a645f3e53bdf0e1020dfcfeac289f698b370b661 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 8 Jun 2026 18:45:54 -0700 Subject: [PATCH 2/6] =?UTF-8?q?fix(sendblue):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20status-aware=20webhook=20dedup,=20shared=20routing?= =?UTF-8?q?=20map,=20uniform=20output=20casing,=20api-key=20authType?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/docs/en/triggers/sendblue.mdx | 4 ++-- apps/sim/lib/integrations/integrations.json | 2 +- apps/sim/lib/webhooks/providers/sendblue.ts | 24 ++++++++----------- apps/sim/triggers/sendblue/utils.ts | 3 ++- scripts/generate-docs.ts | 4 ++++ 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/apps/docs/content/docs/en/triggers/sendblue.mdx b/apps/docs/content/docs/en/triggers/sendblue.mdx index 79a94101aeb..28d14ef6d6f 100644 --- a/apps/docs/content/docs/en/triggers/sendblue.mdx +++ b/apps/docs/content/docs/en/triggers/sendblue.mdx @@ -22,7 +22,7 @@ Trigger when an inbound iMessage or SMS is received in Sendblue | Parameter | Type | Description | | --------- | ---- | ----------- | -| `accountEmail` | string | Email of the Sendblue account | +| `account_email` | string | Email of the Sendblue account | | `content` | string | Message text content | | `media_url` | string | CDN link to attached media, if any | | `is_outbound` | boolean | True for outbound messages, false for inbound | @@ -62,7 +62,7 @@ Trigger when an outbound message status changes (SENT, DELIVERED, ERROR) in Send | Parameter | Type | Description | | --------- | ---- | ----------- | -| `accountEmail` | string | Email of the Sendblue account | +| `account_email` | string | Email of the Sendblue account | | `content` | string | Message text content | | `media_url` | string | CDN link to attached media, if any | | `is_outbound` | boolean | True for outbound messages, false for inbound | diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 2d901910a2b..8ad61e43915 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -12744,7 +12744,7 @@ } ], "triggerCount": 2, - "authType": "none", + "authType": "api-key", "category": "tools", "integrationType": "communication", "tags": ["messaging", "automation", "webhooks"] diff --git a/apps/sim/lib/webhooks/providers/sendblue.ts b/apps/sim/lib/webhooks/providers/sendblue.ts index da8ba930175..b053d37fec7 100644 --- a/apps/sim/lib/webhooks/providers/sendblue.ts +++ b/apps/sim/lib/webhooks/providers/sendblue.ts @@ -6,31 +6,22 @@ import type { FormatInputResult, WebhookProviderHandler, } from '@/lib/webhooks/providers/types' +import { SENDBLUE_TRIGGER_IS_OUTBOUND } from '@/triggers/sendblue/utils' const logger = createLogger('WebhookProvider:Sendblue') -/** - * Maps Sendblue trigger IDs to the expected value of the payload `is_outbound` - * flag. Inbound messages are routed to the "message received" trigger and - * outbound status callbacks to the "message status updated" trigger. - */ -const TRIGGER_IS_OUTBOUND: Record = { - sendblue_message_received: false, - sendblue_message_status_updated: true, -} - export const sendblueHandler: WebhookProviderHandler = { matchEvent({ body, webhook, requestId }: EventMatchContext): boolean { const providerConfig = getProviderConfig(webhook) const triggerId = providerConfig.triggerId as string | undefined - if (!triggerId || !(triggerId in TRIGGER_IS_OUTBOUND)) return true + if (!triggerId || !(triggerId in SENDBLUE_TRIGGER_IS_OUTBOUND)) return true if (!isRecord(body)) { logger.warn(`[${requestId}] Sendblue webhook payload was not an object`) return false } - const expected = TRIGGER_IS_OUTBOUND[triggerId] + const expected = SENDBLUE_TRIGGER_IS_OUTBOUND[triggerId] const isOutbound = body.is_outbound === true if (isOutbound !== expected) { logger.info(`[${requestId}] Sendblue event did not match trigger`, { triggerId, isOutbound }) @@ -43,14 +34,19 @@ export const sendblueHandler: WebhookProviderHandler = { extractIdempotencyId(body: unknown): string | null { if (!isRecord(body)) return null const handle = body.message_handle - return typeof handle === 'string' && handle.length > 0 ? handle : null + if (typeof handle !== 'string' || handle.length === 0) return null + // A single outbound message emits multiple status callbacks (e.g. SENT then + // DELIVERED) that share one message_handle, so the status is part of the key + // to keep distinct transitions from being deduped as retries. + const status = typeof body.status === 'string' && body.status.length > 0 ? body.status : null + return status ? `${handle}:${status}` : handle }, async formatInput({ body }: FormatInputContext): Promise { const b = isRecord(body) ? body : {} return { input: { - accountEmail: b.accountEmail ?? b.account_email ?? null, + account_email: b.accountEmail ?? b.account_email ?? null, content: b.content ?? null, media_url: b.media_url ?? null, is_outbound: b.is_outbound ?? null, diff --git a/apps/sim/triggers/sendblue/utils.ts b/apps/sim/triggers/sendblue/utils.ts index 2bae0a72960..be11280063c 100644 --- a/apps/sim/triggers/sendblue/utils.ts +++ b/apps/sim/triggers/sendblue/utils.ts @@ -3,6 +3,7 @@ import type { TriggerOutput } from '@/triggers/types' /** * Maps Sendblue trigger IDs to the expected value of the webhook payload's * `is_outbound` flag, used to route inbound vs. outbound status events. + * Imported by the webhook provider handler so routing lives in one place. */ export const SENDBLUE_TRIGGER_IS_OUTBOUND: Record = { sendblue_message_received: false, @@ -38,7 +39,7 @@ export function sendblueSetupInstructions(eventType: string): string { */ export function buildSendblueOutputs(): Record { return { - accountEmail: { type: 'string', description: 'Email of the Sendblue account' }, + account_email: { type: 'string', description: 'Email of the Sendblue account' }, content: { type: 'string', description: 'Message text content' }, media_url: { type: 'string', description: 'CDN link to attached media, if any' }, is_outbound: { type: 'boolean', description: 'True for outbound messages, false for inbound' }, diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index c269708133e..a48588445c7 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -535,6 +535,10 @@ async function buildToolDescriptionMap(): Promise { * 'api-key' if it uses a plain API key field, or 'none' otherwise. */ function extractAuthType(blockContent: string): 'oauth' | 'api-key' | 'none' { + // Prefer the authoritative `authMode` declaration when present. + if (/authMode\s*:\s*AuthMode\.OAuth\b/.test(blockContent)) return 'oauth' + if (/authMode\s*:\s*AuthMode\.(?:ApiKey|BotToken)\b/.test(blockContent)) return 'api-key' + // Fall back to credential subBlock heuristics for blocks without authMode. if (/type\s*:\s*['"]oauth-input['"]/.test(blockContent)) return 'oauth' if (/\bid\s*:\s*['"](?:apiKey|api_key|accessToken)['"]/.test(blockContent)) return 'api-key' return 'none' From cb8de5b0ebf84dc03456abc874f7ab845fff09b8 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 8 Jun 2026 18:48:41 -0700 Subject: [PATCH 3/6] fix(docs-gen): skip nested array `items` descriptor in tool input tables --- apps/docs/content/docs/en/tools/sendblue.mdx | 1 - scripts/generate-docs.ts | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/en/tools/sendblue.mdx b/apps/docs/content/docs/en/tools/sendblue.mdx index f25735c70b6..57c30e9f78b 100644 --- a/apps/docs/content/docs/en/tools/sendblue.mdx +++ b/apps/docs/content/docs/en/tools/sendblue.mdx @@ -86,7 +86,6 @@ Send an iMessage or SMS to a group of recipients via Sendblue. | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `numbers` | array | Yes | Recipient phone numbers in E.164 format \(e.g., \["+19998887777", "+13334445555"\]\) | -| `items` | string | No | No description | | `from_number` | string | Yes | One of your registered Sendblue phone numbers to send from, in E.164 format \(e.g., +18887776666\) | | `content` | string | No | Message text content. Either content or media_url must be provided. | | `media_url` | string | No | URL of a media file to send. Either content or media_url must be provided. | diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index a48588445c7..60a40994797 100755 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -1779,6 +1779,9 @@ function extractToolInfo( if (endPos !== -1) { const paramBlock = paramsContent.substring(startPos + 1, endPos - 1).trim() paramPositions.push({ name: paramName, start: startPos, content: paramBlock }) + // Resume scanning after this param's block so nested descriptors + // (e.g. an array param's `items: {...}`) are not parsed as params. + paramBlocksRegex.lastIndex = endPos } } From 842fdfe2efbf7615ef08285d2b34ca6e14383370 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 8 Jun 2026 18:54:39 -0700 Subject: [PATCH 4/6] chore(sendblue): use SendblueSendStyle type, trim URL identifiers --- apps/sim/tools/sendblue/evaluate_service.ts | 2 +- apps/sim/tools/sendblue/get_message.ts | 2 +- apps/sim/tools/sendblue/types.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sim/tools/sendblue/evaluate_service.ts b/apps/sim/tools/sendblue/evaluate_service.ts index 0632f52d799..f56ebf57a39 100644 --- a/apps/sim/tools/sendblue/evaluate_service.ts +++ b/apps/sim/tools/sendblue/evaluate_service.ts @@ -31,7 +31,7 @@ export const sendblueEvaluateServiceTool: ToolConfig< request: { url: (params) => { const url = new URL(`${SENDBLUE_API_BASE_URL}/api/evaluate-service`) - url.searchParams.set('number', params.number) + url.searchParams.set('number', params.number.trim()) return url.toString() }, method: 'GET', diff --git a/apps/sim/tools/sendblue/get_message.ts b/apps/sim/tools/sendblue/get_message.ts index 53713f4cf91..8227807d679 100644 --- a/apps/sim/tools/sendblue/get_message.ts +++ b/apps/sim/tools/sendblue/get_message.ts @@ -27,7 +27,7 @@ export const sendblueGetMessageTool: ToolConfig< request: { url: (params) => - `${SENDBLUE_API_BASE_URL}/api/v2/messages/${encodeURIComponent(params.message_id)}`, + `${SENDBLUE_API_BASE_URL}/api/v2/messages/${encodeURIComponent(params.message_id.trim())}`, method: 'GET', headers: (params) => sendblueHeaders(params), }, diff --git a/apps/sim/tools/sendblue/types.ts b/apps/sim/tools/sendblue/types.ts index 2abcdf9ef82..e025c200b5e 100644 --- a/apps/sim/tools/sendblue/types.ts +++ b/apps/sim/tools/sendblue/types.ts @@ -28,7 +28,7 @@ export interface SendblueSendMessageParams extends SendblueBaseParams { from_number: string content?: string media_url?: string - send_style?: string + send_style?: SendblueSendStyle status_callback?: string } @@ -37,7 +37,7 @@ export interface SendblueSendGroupMessageParams extends SendblueBaseParams { from_number: string content?: string media_url?: string - send_style?: string + send_style?: SendblueSendStyle group_id?: string status_callback?: string } From a7e661ad094ac0689d57c39c14bd0469abb36f69 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 8 Jun 2026 19:20:12 -0700 Subject: [PATCH 5/6] fix(sendblue): keep is_outbound routing map local to the webhook handler Avoids a webhook-providers -> triggers cross-subgraph import (single source of truth in the handler, its only runtime consumer). --- apps/sim/lib/webhooks/providers/sendblue.ts | 13 ++++++++++++- apps/sim/triggers/sendblue/utils.ts | 10 ---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/sim/lib/webhooks/providers/sendblue.ts b/apps/sim/lib/webhooks/providers/sendblue.ts index b053d37fec7..109ed9a8fc3 100644 --- a/apps/sim/lib/webhooks/providers/sendblue.ts +++ b/apps/sim/lib/webhooks/providers/sendblue.ts @@ -6,10 +6,21 @@ import type { FormatInputResult, WebhookProviderHandler, } from '@/lib/webhooks/providers/types' -import { SENDBLUE_TRIGGER_IS_OUTBOUND } from '@/triggers/sendblue/utils' const logger = createLogger('WebhookProvider:Sendblue') +/** + * Maps Sendblue trigger IDs to the expected value of the webhook payload's + * `is_outbound` flag, used to route inbound vs. outbound status events. + * The handler is the only runtime consumer, so the map lives here (single + * source of truth) rather than crossing from the triggers graph into the + * webhook-providers graph. + */ +const SENDBLUE_TRIGGER_IS_OUTBOUND: Record = { + sendblue_message_received: false, + sendblue_message_status_updated: true, +} + export const sendblueHandler: WebhookProviderHandler = { matchEvent({ body, webhook, requestId }: EventMatchContext): boolean { const providerConfig = getProviderConfig(webhook) diff --git a/apps/sim/triggers/sendblue/utils.ts b/apps/sim/triggers/sendblue/utils.ts index be11280063c..3b5849cadc2 100644 --- a/apps/sim/triggers/sendblue/utils.ts +++ b/apps/sim/triggers/sendblue/utils.ts @@ -1,15 +1,5 @@ import type { TriggerOutput } from '@/triggers/types' -/** - * Maps Sendblue trigger IDs to the expected value of the webhook payload's - * `is_outbound` flag, used to route inbound vs. outbound status events. - * Imported by the webhook provider handler so routing lives in one place. - */ -export const SENDBLUE_TRIGGER_IS_OUTBOUND: Record = { - sendblue_message_received: false, - sendblue_message_status_updated: true, -} - export const sendblueTriggerOptions = [ { label: 'Message Received', id: 'sendblue_message_received' }, { label: 'Message Status Updated', id: 'sendblue_message_status_updated' }, From 0b155ad38113322a5d0d50651139a4db1c011fe4 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 8 Jun 2026 19:39:05 -0700 Subject: [PATCH 6/6] fix(sendblue): remove invalid `tags` from block config (belongs on BlockMeta) --- apps/sim/blocks/blocks/sendblue.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/sim/blocks/blocks/sendblue.ts b/apps/sim/blocks/blocks/sendblue.ts index 9b4a105ec5e..ef2993f9ca3 100644 --- a/apps/sim/blocks/blocks/sendblue.ts +++ b/apps/sim/blocks/blocks/sendblue.ts @@ -29,7 +29,6 @@ export const SendblueBlock: BlockConfig = { docsLink: 'https://docs.sim.ai/tools/sendblue', category: 'tools', integrationType: IntegrationType.Communication, - tags: ['messaging', 'automation', 'webhooks'], bgColor: '#008BFF', icon: SendblueIcon, authMode: AuthMode.ApiKey,