Skip to content

Commit 5925c87

Browse files
committed
feat(triggers): add Resend webhook triggers with auto-registration
1 parent 5ca66c3 commit 5925c87

File tree

14 files changed

+731
-0
lines changed

14 files changed

+731
-0
lines changed

apps/sim/blocks/blocks/resend.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ResendIcon } from '@/components/icons'
22
import type { BlockConfig } from '@/blocks/types'
33
import { AuthMode, IntegrationType } from '@/blocks/types'
4+
import { getTrigger } from '@/triggers'
45

56
export const ResendBlock: BlockConfig = {
67
type: 'resend',
@@ -16,6 +17,20 @@ export const ResendBlock: BlockConfig = {
1617
icon: ResendIcon,
1718
authMode: AuthMode.ApiKey,
1819

20+
triggers: {
21+
enabled: true,
22+
available: [
23+
'resend_email_sent',
24+
'resend_email_delivered',
25+
'resend_email_bounced',
26+
'resend_email_complained',
27+
'resend_email_opened',
28+
'resend_email_clicked',
29+
'resend_email_failed',
30+
'resend_webhook',
31+
],
32+
},
33+
1934
subBlocks: [
2035
{
2136
id: 'operation',
@@ -221,6 +236,15 @@ Return ONLY the email body - no explanations, no extra text.`,
221236
condition: { field: 'operation', value: ['get_contact', 'update_contact', 'delete_contact'] },
222237
required: true,
223238
},
239+
240+
...getTrigger('resend_email_sent').subBlocks,
241+
...getTrigger('resend_email_delivered').subBlocks,
242+
...getTrigger('resend_email_bounced').subBlocks,
243+
...getTrigger('resend_email_complained').subBlocks,
244+
...getTrigger('resend_email_opened').subBlocks,
245+
...getTrigger('resend_email_clicked').subBlocks,
246+
...getTrigger('resend_email_failed').subBlocks,
247+
...getTrigger('resend_webhook').subBlocks,
224248
],
225249

226250
tools: {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { lemlistHandler } from '@/lib/webhooks/providers/lemlist'
2121
import { linearHandler } from '@/lib/webhooks/providers/linear'
2222
import { microsoftTeamsHandler } from '@/lib/webhooks/providers/microsoft-teams'
2323
import { outlookHandler } from '@/lib/webhooks/providers/outlook'
24+
import { resendHandler } from '@/lib/webhooks/providers/resend'
2425
import { rssHandler } from '@/lib/webhooks/providers/rss'
2526
import { slackHandler } from '@/lib/webhooks/providers/slack'
2627
import { stripeHandler } from '@/lib/webhooks/providers/stripe'
@@ -55,6 +56,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
5556
jira: jiraHandler,
5657
lemlist: lemlistHandler,
5758
linear: linearHandler,
59+
resend: resendHandler,
5860
'microsoft-teams': microsoftTeamsHandler,
5961
outlook: outlookHandler,
6062
rss: rssHandler,
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { createLogger } from '@sim/logger'
2+
import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/providers/subscription-utils'
3+
import type {
4+
DeleteSubscriptionContext,
5+
FormatInputContext,
6+
FormatInputResult,
7+
SubscriptionContext,
8+
SubscriptionResult,
9+
WebhookProviderHandler,
10+
} from '@/lib/webhooks/providers/types'
11+
12+
const logger = createLogger('WebhookProvider:Resend')
13+
14+
const ALL_RESEND_EVENTS = [
15+
'email.sent',
16+
'email.delivered',
17+
'email.delivery_delayed',
18+
'email.bounced',
19+
'email.complained',
20+
'email.opened',
21+
'email.clicked',
22+
'email.failed',
23+
'email.received',
24+
'email.scheduled',
25+
'email.suppressed',
26+
'contact.created',
27+
'contact.updated',
28+
'contact.deleted',
29+
'domain.created',
30+
'domain.updated',
31+
'domain.deleted',
32+
]
33+
34+
export const resendHandler: WebhookProviderHandler = {
35+
async formatInput({ body }: FormatInputContext): Promise<FormatInputResult> {
36+
const payload = body as Record<string, unknown>
37+
const data = payload.data as Record<string, unknown> | undefined
38+
const bounce = data?.bounce as Record<string, unknown> | undefined
39+
const click = data?.click as Record<string, unknown> | undefined
40+
41+
return {
42+
input: {
43+
type: payload.type,
44+
created_at: payload.created_at,
45+
email_id: data?.email_id ?? null,
46+
from: data?.from ?? null,
47+
to: data?.to ?? null,
48+
subject: data?.subject ?? null,
49+
bounceType: bounce?.type ?? null,
50+
bounceSubType: bounce?.subType ?? null,
51+
bounceMessage: bounce?.message ?? null,
52+
clickIpAddress: click?.ipAddress ?? null,
53+
clickLink: click?.link ?? null,
54+
clickTimestamp: click?.timestamp ?? null,
55+
clickUserAgent: click?.userAgent ?? null,
56+
},
57+
}
58+
},
59+
60+
async createSubscription(ctx: SubscriptionContext): Promise<SubscriptionResult | undefined> {
61+
const { webhook, requestId } = ctx
62+
try {
63+
const providerConfig = getProviderConfig(webhook)
64+
const apiKey = providerConfig.apiKey as string | undefined
65+
const triggerId = providerConfig.triggerId as string | undefined
66+
67+
if (!apiKey) {
68+
logger.warn(`[${requestId}] Missing apiKey for Resend webhook creation.`, {
69+
webhookId: webhook.id,
70+
})
71+
throw new Error(
72+
'Resend API Key is required. Please provide your Resend API Key in the trigger configuration.'
73+
)
74+
}
75+
76+
const eventTypeMap: Record<string, string[]> = {
77+
resend_email_sent: ['email.sent'],
78+
resend_email_delivered: ['email.delivered'],
79+
resend_email_bounced: ['email.bounced'],
80+
resend_email_complained: ['email.complained'],
81+
resend_email_opened: ['email.opened'],
82+
resend_email_clicked: ['email.clicked'],
83+
resend_email_failed: ['email.failed'],
84+
resend_webhook: ALL_RESEND_EVENTS,
85+
}
86+
87+
const events = eventTypeMap[triggerId ?? ''] ?? ALL_RESEND_EVENTS
88+
const notificationUrl = getNotificationUrl(webhook)
89+
90+
logger.info(`[${requestId}] Creating Resend webhook`, {
91+
triggerId,
92+
events,
93+
webhookId: webhook.id,
94+
})
95+
96+
const resendResponse = await fetch('https://api.resend.com/webhooks', {
97+
method: 'POST',
98+
headers: {
99+
Authorization: `Bearer ${apiKey}`,
100+
'Content-Type': 'application/json',
101+
},
102+
body: JSON.stringify({
103+
endpoint: notificationUrl,
104+
events,
105+
}),
106+
})
107+
108+
const responseBody = (await resendResponse.json()) as Record<string, unknown>
109+
110+
if (!resendResponse.ok) {
111+
const errorMessage =
112+
(responseBody.message as string) ||
113+
(responseBody.name as string) ||
114+
'Unknown Resend API error'
115+
logger.error(
116+
`[${requestId}] Failed to create webhook in Resend for webhook ${webhook.id}. Status: ${resendResponse.status}`,
117+
{ message: errorMessage, response: responseBody }
118+
)
119+
120+
let userFriendlyMessage = 'Failed to create webhook subscription in Resend'
121+
if (resendResponse.status === 401 || resendResponse.status === 403) {
122+
userFriendlyMessage = 'Invalid Resend API Key. Please verify your API Key is correct.'
123+
} else if (errorMessage && errorMessage !== 'Unknown Resend API error') {
124+
userFriendlyMessage = `Resend error: ${errorMessage}`
125+
}
126+
127+
throw new Error(userFriendlyMessage)
128+
}
129+
130+
logger.info(
131+
`[${requestId}] Successfully created webhook in Resend for webhook ${webhook.id}.`,
132+
{
133+
resendWebhookId: responseBody.id,
134+
}
135+
)
136+
137+
return { providerConfigUpdates: { externalId: responseBody.id } }
138+
} catch (error: unknown) {
139+
const err = error as Error
140+
logger.error(
141+
`[${requestId}] Exception during Resend webhook creation for webhook ${webhook.id}.`,
142+
{
143+
message: err.message,
144+
stack: err.stack,
145+
}
146+
)
147+
throw error
148+
}
149+
},
150+
151+
async deleteSubscription(ctx: DeleteSubscriptionContext): Promise<void> {
152+
const { webhook, requestId } = ctx
153+
try {
154+
const config = getProviderConfig(webhook)
155+
const apiKey = config.apiKey as string | undefined
156+
const externalId = config.externalId as string | undefined
157+
158+
if (!apiKey || !externalId) {
159+
logger.warn(
160+
`[${requestId}] Missing apiKey or externalId for Resend webhook deletion ${webhook.id}, skipping cleanup`
161+
)
162+
return
163+
}
164+
165+
const resendResponse = await fetch(`https://api.resend.com/webhooks/${externalId}`, {
166+
method: 'DELETE',
167+
headers: {
168+
Authorization: `Bearer ${apiKey}`,
169+
},
170+
})
171+
172+
if (!resendResponse.ok && resendResponse.status !== 404) {
173+
const responseBody = await resendResponse.json().catch(() => ({}))
174+
logger.warn(
175+
`[${requestId}] Failed to delete Resend webhook (non-fatal): ${resendResponse.status}`,
176+
{ response: responseBody }
177+
)
178+
} else {
179+
logger.info(`[${requestId}] Successfully deleted Resend webhook ${externalId}`)
180+
}
181+
} catch (error) {
182+
logger.warn(`[${requestId}] Error deleting Resend webhook (non-fatal)`, error)
183+
}
184+
},
185+
}

apps/sim/triggers/registry.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ import {
162162
microsoftTeamsWebhookTrigger,
163163
} from '@/triggers/microsoftteams'
164164
import { outlookPollingTrigger } from '@/triggers/outlook'
165+
import {
166+
resendEmailBouncedTrigger,
167+
resendEmailClickedTrigger,
168+
resendEmailComplainedTrigger,
169+
resendEmailDeliveredTrigger,
170+
resendEmailFailedTrigger,
171+
resendEmailOpenedTrigger,
172+
resendEmailSentTrigger,
173+
resendWebhookTrigger,
174+
} from '@/triggers/resend'
165175
import { rssPollingTrigger } from '@/triggers/rss'
166176
import { slackWebhookTrigger } from '@/triggers/slack'
167177
import { stripeWebhookTrigger } from '@/triggers/stripe'
@@ -298,6 +308,14 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
298308
microsoftteams_webhook: microsoftTeamsWebhookTrigger,
299309
microsoftteams_chat_subscription: microsoftTeamsChatSubscriptionTrigger,
300310
outlook_poller: outlookPollingTrigger,
311+
resend_email_sent: resendEmailSentTrigger,
312+
resend_email_delivered: resendEmailDeliveredTrigger,
313+
resend_email_bounced: resendEmailBouncedTrigger,
314+
resend_email_complained: resendEmailComplainedTrigger,
315+
resend_email_opened: resendEmailOpenedTrigger,
316+
resend_email_clicked: resendEmailClickedTrigger,
317+
resend_email_failed: resendEmailFailedTrigger,
318+
resend_webhook: resendWebhookTrigger,
301319
rss_poller: rssPollingTrigger,
302320
stripe_webhook: stripeWebhookTrigger,
303321
telegram_webhook: telegramWebhookTrigger,
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ResendIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildEmailBouncedOutputs,
5+
buildResendExtraFields,
6+
resendSetupInstructions,
7+
resendTriggerOptions,
8+
} from '@/triggers/resend/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Resend Email Bounced Trigger
13+
* Triggers when an email permanently bounces.
14+
*/
15+
export const resendEmailBouncedTrigger: TriggerConfig = {
16+
id: 'resend_email_bounced',
17+
name: 'Resend Email Bounced',
18+
provider: 'resend',
19+
description: 'Trigger workflow when an email bounces',
20+
version: '1.0.0',
21+
icon: ResendIcon,
22+
23+
subBlocks: buildTriggerSubBlocks({
24+
triggerId: 'resend_email_bounced',
25+
triggerOptions: resendTriggerOptions,
26+
setupInstructions: resendSetupInstructions('email.bounced'),
27+
extraFields: buildResendExtraFields('resend_email_bounced'),
28+
}),
29+
30+
outputs: buildEmailBouncedOutputs(),
31+
32+
webhook: {
33+
method: 'POST',
34+
headers: {
35+
'Content-Type': 'application/json',
36+
},
37+
},
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ResendIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildEmailClickedOutputs,
5+
buildResendExtraFields,
6+
resendSetupInstructions,
7+
resendTriggerOptions,
8+
} from '@/triggers/resend/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Resend Email Clicked Trigger
13+
* Triggers when a recipient clicks a link in an email.
14+
*/
15+
export const resendEmailClickedTrigger: TriggerConfig = {
16+
id: 'resend_email_clicked',
17+
name: 'Resend Email Clicked',
18+
provider: 'resend',
19+
description: 'Trigger workflow when a link in an email is clicked',
20+
version: '1.0.0',
21+
icon: ResendIcon,
22+
23+
subBlocks: buildTriggerSubBlocks({
24+
triggerId: 'resend_email_clicked',
25+
triggerOptions: resendTriggerOptions,
26+
setupInstructions: resendSetupInstructions('email.clicked'),
27+
extraFields: buildResendExtraFields('resend_email_clicked'),
28+
}),
29+
30+
outputs: buildEmailClickedOutputs(),
31+
32+
webhook: {
33+
method: 'POST',
34+
headers: {
35+
'Content-Type': 'application/json',
36+
},
37+
},
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ResendIcon } from '@/components/icons'
2+
import { buildTriggerSubBlocks } from '@/triggers'
3+
import {
4+
buildEmailComplainedOutputs,
5+
buildResendExtraFields,
6+
resendSetupInstructions,
7+
resendTriggerOptions,
8+
} from '@/triggers/resend/utils'
9+
import type { TriggerConfig } from '@/triggers/types'
10+
11+
/**
12+
* Resend Email Complained Trigger
13+
* Triggers when a recipient marks an email as spam.
14+
*/
15+
export const resendEmailComplainedTrigger: TriggerConfig = {
16+
id: 'resend_email_complained',
17+
name: 'Resend Email Complained',
18+
provider: 'resend',
19+
description: 'Trigger workflow when an email is marked as spam',
20+
version: '1.0.0',
21+
icon: ResendIcon,
22+
23+
subBlocks: buildTriggerSubBlocks({
24+
triggerId: 'resend_email_complained',
25+
triggerOptions: resendTriggerOptions,
26+
setupInstructions: resendSetupInstructions('email.complained'),
27+
extraFields: buildResendExtraFields('resend_email_complained'),
28+
}),
29+
30+
outputs: buildEmailComplainedOutputs(),
31+
32+
webhook: {
33+
method: 'POST',
34+
headers: {
35+
'Content-Type': 'application/json',
36+
},
37+
},
38+
}

0 commit comments

Comments
 (0)