@@ -19,6 +19,7 @@ const calendlyLogger = createLogger('CalendlyWebhook')
1919const grainLogger = createLogger ( 'GrainWebhook' )
2020const lemlistLogger = createLogger ( 'LemlistWebhook' )
2121const webflowLogger = createLogger ( 'WebflowWebhook' )
22+ const attioLogger = createLogger ( 'AttioWebhook' )
2223const providerSubscriptionsLogger = createLogger ( 'WebhookProviderSubscriptions' )
2324
2425function getProviderConfig ( webhook : any ) : Record < string , any > {
@@ -976,6 +977,196 @@ export async function deleteWebflowWebhook(
976977 }
977978}
978979
980+ export async function createAttioWebhookSubscription (
981+ userId : string ,
982+ webhookData : any ,
983+ requestId : string
984+ ) : Promise < { externalId : string ; webhookSecret : string } | undefined > {
985+ try {
986+ const { path, providerConfig } = webhookData
987+ const { triggerId, credentialId } = providerConfig || { }
988+
989+ if ( ! credentialId ) {
990+ attioLogger . warn ( `[${ requestId } ] Missing credentialId for Attio webhook creation.` , {
991+ webhookId : webhookData . id ,
992+ } )
993+ throw new Error (
994+ 'Attio account connection required. Please connect your Attio account in the trigger configuration and try again.'
995+ )
996+ }
997+
998+ const credentialOwner = await getCredentialOwner ( credentialId , requestId )
999+ const accessToken = credentialOwner
1000+ ? await refreshAccessTokenIfNeeded (
1001+ credentialOwner . accountId ,
1002+ credentialOwner . userId ,
1003+ requestId
1004+ )
1005+ : null
1006+
1007+ if ( ! accessToken ) {
1008+ attioLogger . warn (
1009+ `[${ requestId } ] Could not retrieve Attio access token for user ${ userId } . Cannot create webhook.`
1010+ )
1011+ throw new Error (
1012+ 'Attio account connection required. Please connect your Attio account in the trigger configuration and try again.'
1013+ )
1014+ }
1015+
1016+ const notificationUrl = `${ getBaseUrl ( ) } /api/webhooks/trigger/${ path } `
1017+
1018+ const { TRIGGER_EVENT_MAP } = await import ( '@/triggers/attio/utils' )
1019+
1020+ let subscriptions : Array < { event_type : string } > = [ ]
1021+ if ( triggerId === 'attio_webhook' ) {
1022+ const allEvents = new Set < string > ( )
1023+ for ( const events of Object . values ( TRIGGER_EVENT_MAP ) ) {
1024+ for ( const event of events ) {
1025+ allEvents . add ( event )
1026+ }
1027+ }
1028+ subscriptions = Array . from ( allEvents ) . map ( ( event_type ) => ( { event_type } ) )
1029+ } else {
1030+ const events = TRIGGER_EVENT_MAP [ triggerId ]
1031+ if ( ! events || events . length === 0 ) {
1032+ attioLogger . warn ( `[${ requestId } ] No event types mapped for trigger ${ triggerId } ` , {
1033+ webhookId : webhookData . id ,
1034+ } )
1035+ throw new Error ( `Unknown Attio trigger type: ${ triggerId } ` )
1036+ }
1037+ subscriptions = events . map ( ( event_type ) => ( { event_type } ) )
1038+ }
1039+
1040+ const requestBody = {
1041+ target_url : notificationUrl ,
1042+ subscriptions,
1043+ }
1044+
1045+ const attioResponse = await fetch ( 'https://api.attio.com/v2/webhooks' , {
1046+ method : 'POST' ,
1047+ headers : {
1048+ Authorization : `Bearer ${ accessToken } ` ,
1049+ 'Content-Type' : 'application/json' ,
1050+ } ,
1051+ body : JSON . stringify ( requestBody ) ,
1052+ } )
1053+
1054+ if ( ! attioResponse . ok ) {
1055+ const errorBody = await attioResponse . json ( ) . catch ( ( ) => ( { } ) )
1056+ attioLogger . error (
1057+ `[${ requestId } ] Failed to create webhook in Attio for webhook ${ webhookData . id } . Status: ${ attioResponse . status } ` ,
1058+ { response : errorBody }
1059+ )
1060+
1061+ let userFriendlyMessage = 'Failed to create webhook subscription in Attio'
1062+ if ( attioResponse . status === 401 ) {
1063+ userFriendlyMessage = 'Attio authentication failed. Please reconnect your Attio account.'
1064+ } else if ( attioResponse . status === 403 ) {
1065+ userFriendlyMessage =
1066+ 'Attio access denied. Please ensure your integration has webhook permissions.'
1067+ }
1068+
1069+ throw new Error ( userFriendlyMessage )
1070+ }
1071+
1072+ const responseBody = await attioResponse . json ( )
1073+ const data = responseBody . data || responseBody
1074+ const webhookId = data . id ?. webhook_id || data . webhook_id || data . id
1075+ const secret = data . secret
1076+
1077+ if ( ! webhookId ) {
1078+ attioLogger . error (
1079+ `[${ requestId } ] Attio webhook created but no webhook_id returned for webhook ${ webhookData . id } ` ,
1080+ { response : responseBody }
1081+ )
1082+ throw new Error ( 'Attio webhook creation succeeded but no webhook ID was returned' )
1083+ }
1084+
1085+ if ( ! secret ) {
1086+ attioLogger . warn (
1087+ `[${ requestId } ] Attio webhook created but no secret returned for webhook ${ webhookData . id } . Signature verification will be skipped.` ,
1088+ { response : responseBody }
1089+ )
1090+ }
1091+
1092+ attioLogger . info (
1093+ `[${ requestId } ] Successfully created webhook in Attio for webhook ${ webhookData . id } .` ,
1094+ { attioWebhookId : webhookId }
1095+ )
1096+
1097+ return { externalId : webhookId , webhookSecret : secret || '' }
1098+ } catch ( error : unknown ) {
1099+ const message = error instanceof Error ? error . message : String ( error )
1100+ attioLogger . error (
1101+ `[${ requestId } ] Exception during Attio webhook creation for webhook ${ webhookData . id } .` ,
1102+ { message }
1103+ )
1104+ throw error
1105+ }
1106+ }
1107+
1108+ export async function deleteAttioWebhook (
1109+ webhook : any ,
1110+ _workflow : any ,
1111+ requestId : string
1112+ ) : Promise < void > {
1113+ try {
1114+ const config = getProviderConfig ( webhook )
1115+ const externalId = config . externalId as string | undefined
1116+ const credentialId = config . credentialId as string | undefined
1117+
1118+ if ( ! externalId ) {
1119+ attioLogger . warn (
1120+ `[${ requestId } ] Missing externalId for Attio webhook deletion ${ webhook . id } , skipping cleanup`
1121+ )
1122+ return
1123+ }
1124+
1125+ if ( ! credentialId ) {
1126+ attioLogger . warn (
1127+ `[${ requestId } ] Missing credentialId for Attio webhook deletion ${ webhook . id } , skipping cleanup`
1128+ )
1129+ return
1130+ }
1131+
1132+ const credentialOwner = await getCredentialOwner ( credentialId , requestId )
1133+ const accessToken = credentialOwner
1134+ ? await refreshAccessTokenIfNeeded (
1135+ credentialOwner . accountId ,
1136+ credentialOwner . userId ,
1137+ requestId
1138+ )
1139+ : null
1140+
1141+ if ( ! accessToken ) {
1142+ attioLogger . warn (
1143+ `[${ requestId } ] Could not retrieve Attio access token. Cannot delete webhook.` ,
1144+ { webhookId : webhook . id }
1145+ )
1146+ return
1147+ }
1148+
1149+ const attioResponse = await fetch ( `https://api.attio.com/v2/webhooks/${ externalId } ` , {
1150+ method : 'DELETE' ,
1151+ headers : {
1152+ Authorization : `Bearer ${ accessToken } ` ,
1153+ } ,
1154+ } )
1155+
1156+ if ( ! attioResponse . ok && attioResponse . status !== 404 ) {
1157+ const responseBody = await attioResponse . json ( ) . catch ( ( ) => ( { } ) )
1158+ attioLogger . warn (
1159+ `[${ requestId } ] Failed to delete Attio webhook (non-fatal): ${ attioResponse . status } ` ,
1160+ { response : responseBody }
1161+ )
1162+ } else {
1163+ attioLogger . info ( `[${ requestId } ] Successfully deleted Attio webhook ${ externalId } ` )
1164+ }
1165+ } catch ( error ) {
1166+ attioLogger . warn ( `[${ requestId } ] Error deleting Attio webhook (non-fatal)` , error )
1167+ }
1168+ }
1169+
9791170export async function createGrainWebhookSubscription (
9801171 _request : NextRequest ,
9811172 webhookData : any ,
@@ -1611,6 +1802,7 @@ type RecreateCheckInput = {
16111802/** Providers that create external webhook subscriptions */
16121803const PROVIDERS_WITH_EXTERNAL_SUBSCRIPTIONS = new Set ( [
16131804 'airtable' ,
1805+ 'attio' ,
16141806 'calendly' ,
16151807 'webflow' ,
16161808 'typeform' ,
@@ -1626,6 +1818,7 @@ const SYSTEM_MANAGED_FIELDS = new Set([
16261818 'externalSubscriptionId' ,
16271819 'eventTypes' ,
16281820 'webhookTag' ,
1821+ 'webhookSecret' ,
16291822 'historyId' ,
16301823 'lastCheckedTimestamp' ,
16311824 'setupCompleted' ,
@@ -1686,6 +1879,16 @@ export async function createExternalWebhookSubscription(
16861879 updatedProviderConfig = { ...updatedProviderConfig , externalId }
16871880 externalSubscriptionCreated = true
16881881 }
1882+ } else if ( provider === 'attio' ) {
1883+ const result = await createAttioWebhookSubscription ( userId , webhookData , requestId )
1884+ if ( result ) {
1885+ updatedProviderConfig = {
1886+ ...updatedProviderConfig ,
1887+ externalId : result . externalId ,
1888+ webhookSecret : result . webhookSecret ,
1889+ }
1890+ externalSubscriptionCreated = true
1891+ }
16891892 } else if ( provider === 'calendly' ) {
16901893 const externalId = await createCalendlyWebhookSubscription ( webhookData , requestId )
16911894 if ( externalId ) {
@@ -1736,7 +1939,7 @@ export async function createExternalWebhookSubscription(
17361939
17371940/**
17381941 * Clean up external webhook subscriptions for a webhook
1739- * Handles Airtable, Teams, Telegram, Typeform, Calendly, Grain, and Lemlist cleanup
1942+ * Handles Airtable, Attio, Teams, Telegram, Typeform, Calendly, Grain, and Lemlist cleanup
17401943 * Don't fail deletion if cleanup fails
17411944 */
17421945export async function cleanupExternalWebhook (
@@ -1746,6 +1949,8 @@ export async function cleanupExternalWebhook(
17461949) : Promise < void > {
17471950 if ( webhook . provider === 'airtable' ) {
17481951 await deleteAirtableWebhook ( webhook , workflow , requestId )
1952+ } else if ( webhook . provider === 'attio' ) {
1953+ await deleteAttioWebhook ( webhook , workflow , requestId )
17491954 } else if ( webhook . provider === 'microsoft-teams' ) {
17501955 await deleteTeamsSubscription ( webhook , workflow , requestId )
17511956 } else if ( webhook . provider === 'telegram' ) {
0 commit comments