Skip to content

Commit 590f376

Browse files
authored
feat(triggers): add Intercom webhook triggers (#3990)
* feat(triggers): add Intercom webhook triggers * fix(triggers): address PR review feedback for Intercom triggers
1 parent 62ea0f1 commit 590f376

File tree

13 files changed

+570
-2
lines changed

13 files changed

+570
-2
lines changed

apps/sim/app/(landing)/integrations/data/integrations.json

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6088,8 +6088,39 @@
60886088
}
60896089
],
60906090
"operationCount": 31,
6091-
"triggers": [],
6092-
"triggerCount": 0,
6091+
"triggers": [
6092+
{
6093+
"id": "intercom_conversation_created",
6094+
"name": "Intercom Conversation Created",
6095+
"description": "Trigger workflow when a new conversation is created in Intercom"
6096+
},
6097+
{
6098+
"id": "intercom_conversation_reply",
6099+
"name": "Intercom Conversation Reply",
6100+
"description": "Trigger workflow when someone replies to an Intercom conversation"
6101+
},
6102+
{
6103+
"id": "intercom_conversation_closed",
6104+
"name": "Intercom Conversation Closed",
6105+
"description": "Trigger workflow when a conversation is closed in Intercom"
6106+
},
6107+
{
6108+
"id": "intercom_contact_created",
6109+
"name": "Intercom Contact Created",
6110+
"description": "Trigger workflow when a new lead is created in Intercom"
6111+
},
6112+
{
6113+
"id": "intercom_user_created",
6114+
"name": "Intercom User Created",
6115+
"description": "Trigger workflow when a new user is created in Intercom"
6116+
},
6117+
{
6118+
"id": "intercom_webhook",
6119+
"name": "Intercom Webhook (All Events)",
6120+
"description": "Trigger workflow on any Intercom webhook event"
6121+
}
6122+
],
6123+
"triggerCount": 6,
60936124
"authType": "api-key",
60946125
"category": "tools",
60956126
"integrationType": "customer-support",

apps/sim/blocks/blocks/intercom.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { IntercomIcon } from '@/components/icons'
22
import type { BlockConfig } from '@/blocks/types'
33
import { AuthMode, IntegrationType } from '@/blocks/types'
44
import { createVersionedToolSelector } from '@/blocks/utils'
5+
import { getTrigger } from '@/triggers'
56

67
export const IntercomBlock: BlockConfig = {
78
type: 'intercom',
@@ -1409,6 +1410,26 @@ export const IntercomV2Block: BlockConfig = {
14091410
integrationType: IntegrationType.CustomerSupport,
14101411
tags: ['customer-support', 'messaging'],
14111412
hideFromToolbar: false,
1413+
subBlocks: [
1414+
...IntercomBlock.subBlocks,
1415+
...getTrigger('intercom_conversation_created').subBlocks,
1416+
...getTrigger('intercom_conversation_reply').subBlocks,
1417+
...getTrigger('intercom_conversation_closed').subBlocks,
1418+
...getTrigger('intercom_contact_created').subBlocks,
1419+
...getTrigger('intercom_user_created').subBlocks,
1420+
...getTrigger('intercom_webhook').subBlocks,
1421+
],
1422+
triggers: {
1423+
enabled: true,
1424+
available: [
1425+
'intercom_conversation_created',
1426+
'intercom_conversation_reply',
1427+
'intercom_conversation_closed',
1428+
'intercom_contact_created',
1429+
'intercom_user_created',
1430+
'intercom_webhook',
1431+
],
1432+
},
14121433
tools: {
14131434
...IntercomBlock.tools,
14141435
access: [
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import crypto from 'crypto'
2+
import { createLogger } from '@sim/logger'
3+
import { NextResponse } from 'next/server'
4+
import { safeCompare } from '@/lib/core/security/encryption'
5+
import type {
6+
AuthContext,
7+
EventMatchContext,
8+
FormatInputContext,
9+
FormatInputResult,
10+
WebhookProviderHandler,
11+
} from '@/lib/webhooks/providers/types'
12+
13+
const logger = createLogger('WebhookProvider:Intercom')
14+
15+
/**
16+
* Validate Intercom webhook signature using HMAC-SHA1.
17+
* Intercom signs payloads with the app's Client Secret and sends the
18+
* signature in the X-Hub-Signature header as "sha1=<hex>".
19+
*/
20+
function validateIntercomSignature(secret: string, signature: string, body: string): boolean {
21+
try {
22+
if (!secret || !signature || !body) {
23+
logger.warn('Intercom signature validation missing required fields', {
24+
hasSecret: !!secret,
25+
hasSignature: !!signature,
26+
hasBody: !!body,
27+
})
28+
return false
29+
}
30+
31+
if (!signature.startsWith('sha1=')) {
32+
logger.warn('Intercom signature has invalid format', {
33+
signature: `${signature.substring(0, 10)}...`,
34+
})
35+
return false
36+
}
37+
38+
const providedSignature = signature.substring(5)
39+
const computedHash = crypto.createHmac('sha1', secret).update(body, 'utf8').digest('hex')
40+
41+
return safeCompare(computedHash, providedSignature)
42+
} catch (error) {
43+
logger.error('Error validating Intercom signature:', error)
44+
return false
45+
}
46+
}
47+
48+
export const intercomHandler: WebhookProviderHandler = {
49+
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
50+
const secret = providerConfig.webhookSecret as string | undefined
51+
if (!secret) {
52+
return null
53+
}
54+
55+
const signature = request.headers.get('X-Hub-Signature')
56+
if (!signature) {
57+
logger.warn(`[${requestId}] Intercom webhook missing X-Hub-Signature header`)
58+
return new NextResponse('Unauthorized - Missing Intercom signature', { status: 401 })
59+
}
60+
61+
if (!validateIntercomSignature(secret, signature, rawBody)) {
62+
logger.warn(`[${requestId}] Intercom signature verification failed`, {
63+
signatureLength: signature.length,
64+
secretLength: secret.length,
65+
})
66+
return new NextResponse('Unauthorized - Invalid Intercom signature', { status: 401 })
67+
}
68+
69+
return null
70+
},
71+
72+
handleReachabilityTest(body: unknown, requestId: string) {
73+
const obj = body as Record<string, unknown> | null
74+
if (obj?.topic === 'ping') {
75+
logger.info(
76+
`[${requestId}] Intercom ping event detected - returning 200 without triggering workflow`
77+
)
78+
return NextResponse.json({
79+
status: 'ok',
80+
message: 'Webhook endpoint verified',
81+
})
82+
}
83+
return null
84+
},
85+
86+
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
87+
return { input: body }
88+
},
89+
90+
async matchEvent({ webhook, body, requestId, providerConfig }: EventMatchContext) {
91+
const triggerId = providerConfig.triggerId as string | undefined
92+
const obj = body as Record<string, unknown>
93+
const topic = obj?.topic as string | undefined
94+
95+
if (triggerId && triggerId !== 'intercom_webhook') {
96+
const { isIntercomEventMatch } = await import('@/triggers/intercom/utils')
97+
if (!isIntercomEventMatch(triggerId, topic || '')) {
98+
logger.debug(
99+
`[${requestId}] Intercom event mismatch for trigger ${triggerId}. Topic: ${topic}. Skipping execution.`,
100+
{
101+
webhookId: webhook.id,
102+
triggerId,
103+
receivedTopic: topic,
104+
}
105+
)
106+
return false
107+
}
108+
}
109+
110+
return true
111+
},
112+
113+
extractIdempotencyId(body: unknown) {
114+
const obj = body as Record<string, unknown>
115+
if (obj?.id && obj?.type === 'notification_event') {
116+
return String(obj.id)
117+
}
118+
return null
119+
},
120+
}

apps/sim/lib/webhooks/providers/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { googleFormsHandler } from '@/lib/webhooks/providers/google-forms'
1717
import { grainHandler } from '@/lib/webhooks/providers/grain'
1818
import { hubspotHandler } from '@/lib/webhooks/providers/hubspot'
1919
import { imapHandler } from '@/lib/webhooks/providers/imap'
20+
import { intercomHandler } from '@/lib/webhooks/providers/intercom'
2021
import { jiraHandler } from '@/lib/webhooks/providers/jira'
2122
import { lemlistHandler } from '@/lib/webhooks/providers/lemlist'
2223
import { linearHandler } from '@/lib/webhooks/providers/linear'
@@ -55,6 +56,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
5556
grain: grainHandler,
5657
hubspot: hubspotHandler,
5758
imap: imapHandler,
59+
intercom: intercomHandler,
5860
jira: jiraHandler,
5961
lemlist: lemlistHandler,
6062
linear: linearHandler,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { IntercomIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildIntercomContactOutputs,
5+
buildIntercomExtraFields,
6+
intercomSetupInstructions,
7+
intercomTriggerOptions,
8+
} from '@/triggers/intercom/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Intercom Contact Created Trigger
13+
*
14+
* Fires when a new lead is created in Intercom.
15+
* Note: In Intercom, contact.created fires for leads only.
16+
* For identified users, use the User Created trigger (user.created topic).
17+
*/
18+
export const intercomContactCreatedTrigger: TriggerConfig = {
19+
id: 'intercom_contact_created',
20+
name: 'Intercom Contact Created',
21+
provider: 'intercom',
22+
description: 'Trigger workflow when a new lead is created in Intercom',
23+
version: '1.0.0',
24+
icon: IntercomIcon,
25+
26+
subBlocks: buildTriggerSubBlocks({
27+
triggerId: 'intercom_contact_created',
28+
triggerOptions: intercomTriggerOptions,
29+
setupInstructions: intercomSetupInstructions('contact.created'),
30+
extraFields: buildIntercomExtraFields('intercom_contact_created'),
31+
}),
32+
33+
outputs: buildIntercomContactOutputs(),
34+
35+
webhook: {
36+
method: 'POST',
37+
headers: {
38+
'Content-Type': 'application/json',
39+
},
40+
},
41+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { IntercomIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildIntercomConversationOutputs,
5+
buildIntercomExtraFields,
6+
intercomSetupInstructions,
7+
intercomTriggerOptions,
8+
} from '@/triggers/intercom/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Intercom Conversation Closed Trigger
13+
*
14+
* Fires when an admin closes a conversation.
15+
*/
16+
export const intercomConversationClosedTrigger: TriggerConfig = {
17+
id: 'intercom_conversation_closed',
18+
name: 'Intercom Conversation Closed',
19+
provider: 'intercom',
20+
description: 'Trigger workflow when a conversation is closed in Intercom',
21+
version: '1.0.0',
22+
icon: IntercomIcon,
23+
24+
subBlocks: buildTriggerSubBlocks({
25+
triggerId: 'intercom_conversation_closed',
26+
triggerOptions: intercomTriggerOptions,
27+
setupInstructions: intercomSetupInstructions('conversation.admin.closed'),
28+
extraFields: buildIntercomExtraFields('intercom_conversation_closed'),
29+
}),
30+
31+
outputs: buildIntercomConversationOutputs(),
32+
33+
webhook: {
34+
method: 'POST',
35+
headers: {
36+
'Content-Type': 'application/json',
37+
},
38+
},
39+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { IntercomIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildIntercomConversationOutputs,
5+
buildIntercomExtraFields,
6+
intercomSetupInstructions,
7+
intercomTriggerOptions,
8+
} from '@/triggers/intercom/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Intercom Conversation Created Trigger
13+
*
14+
* This is the PRIMARY trigger - it includes the dropdown for selecting trigger type.
15+
* Fires when a user/lead starts a new conversation or an admin initiates a 1:1 conversation.
16+
*/
17+
export const intercomConversationCreatedTrigger: TriggerConfig = {
18+
id: 'intercom_conversation_created',
19+
name: 'Intercom Conversation Created',
20+
provider: 'intercom',
21+
description: 'Trigger workflow when a new conversation is created in Intercom',
22+
version: '1.0.0',
23+
icon: IntercomIcon,
24+
25+
subBlocks: buildTriggerSubBlocks({
26+
triggerId: 'intercom_conversation_created',
27+
triggerOptions: intercomTriggerOptions,
28+
includeDropdown: true,
29+
setupInstructions: intercomSetupInstructions(
30+
'conversation.user.created and/or conversation.admin.single.created'
31+
),
32+
extraFields: buildIntercomExtraFields('intercom_conversation_created'),
33+
}),
34+
35+
outputs: buildIntercomConversationOutputs(),
36+
37+
webhook: {
38+
method: 'POST',
39+
headers: {
40+
'Content-Type': 'application/json',
41+
},
42+
},
43+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { IntercomIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildIntercomConversationOutputs,
5+
buildIntercomExtraFields,
6+
intercomSetupInstructions,
7+
intercomTriggerOptions,
8+
} from '@/triggers/intercom/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Intercom Conversation Reply Trigger
13+
*
14+
* Fires when a user, lead, or admin replies to a conversation.
15+
*/
16+
export const intercomConversationReplyTrigger: TriggerConfig = {
17+
id: 'intercom_conversation_reply',
18+
name: 'Intercom Conversation Reply',
19+
provider: 'intercom',
20+
description: 'Trigger workflow when someone replies to an Intercom conversation',
21+
version: '1.0.0',
22+
icon: IntercomIcon,
23+
24+
subBlocks: buildTriggerSubBlocks({
25+
triggerId: 'intercom_conversation_reply',
26+
triggerOptions: intercomTriggerOptions,
27+
setupInstructions: intercomSetupInstructions(
28+
'conversation.user.replied and/or conversation.admin.replied'
29+
),
30+
extraFields: buildIntercomExtraFields('intercom_conversation_reply'),
31+
}),
32+
33+
outputs: buildIntercomConversationOutputs(),
34+
35+
webhook: {
36+
method: 'POST',
37+
headers: {
38+
'Content-Type': 'application/json',
39+
},
40+
},
41+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export { intercomContactCreatedTrigger } from './contact_created'
2+
export { intercomConversationClosedTrigger } from './conversation_closed'
3+
export { intercomConversationCreatedTrigger } from './conversation_created'
4+
export { intercomConversationReplyTrigger } from './conversation_reply'
5+
export { intercomUserCreatedTrigger } from './user_created'
6+
export { intercomWebhookTrigger } from './webhook'

0 commit comments

Comments
 (0)