Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,139 changes: 22 additions & 1,117 deletions apps/docs/content/docs/en/triggers/hubspot.mdx

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions apps/sim/app/api/tools/hubspot/lists/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { hubspotListsSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('HubSpotListsAPI')

interface HubSpotList {
listId: string
name: string
objectTypeId?: string
processingType?: string
deletedAt?: string | null
}

export const GET = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()

try {
const parsed = await parseRequest(hubspotListsSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credentialId, objectTypeId, query } = parsed.data.query

const authz = await authorizeCredentialUse(request, {
credentialId,
requireWorkflowIdForInternal: false,
})
if (!authz.ok || !authz.credentialOwnerUserId || !authz.resolvedCredentialId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}

const params = new URLSearchParams()
if (objectTypeId) params.set('objectTypeId', objectTypeId as string)
params.set('count', '500')

const response = await fetch(
`https://api.hubapi.com/crm/v3/lists/search?${params.toString()}`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: '',
processingTypes: ['MANUAL', 'DYNAMIC', 'SNAPSHOT'],
...(objectTypeId ? { additionalProperties: ['hs_object_id'] } : {}),
}),
}
)

if (!response.ok) {
const errorText = await response.text().catch(() => '')
logger.error(`[${requestId}] HubSpot lists API error ${response.status}: ${errorText}`)
return NextResponse.json(
{ error: errorText || 'Failed to fetch HubSpot lists' },
{ status: response.status }
)
}

const data = (await response.json()) as { lists?: HubSpotList[] }
const filterTerm = (query as string | undefined)?.toLowerCase()
const lists = (data.lists ?? [])
.filter((l) => !l.deletedAt)
.map((l) => ({
id: l.listId,
name: l.name,
objectType: l.objectTypeId,
processingType: l.processingType,
}))
.filter(
(l) =>
!filterTerm ||
l.id.toLowerCase().includes(filterTerm) ||
l.name.toLowerCase().includes(filterTerm)
)
.sort((a, b) => a.name.localeCompare(b.name))

return NextResponse.json({ lists }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching HubSpot lists:`, error)
return NextResponse.json({ error: 'Failed to fetch HubSpot lists' }, { status: 500 })
}
})
96 changes: 96 additions & 0 deletions apps/sim/app/api/tools/hubspot/owners/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { hubspotOwnersSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('HubSpotOwnersAPI')

interface HubSpotOwner {
id: string
email?: string
firstName?: string
lastName?: string
archived?: boolean
}

export const GET = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()

try {
const parsed = await parseRequest(hubspotOwnersSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credentialId, query } = parsed.data.query

const authz = await authorizeCredentialUse(request, {
credentialId,
requireWorkflowIdForInternal: false,
})
if (!authz.ok || !authz.credentialOwnerUserId || !authz.resolvedCredentialId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}

const collected: HubSpotOwner[] = []
let after: string | undefined
let pages = 0
do {
const params = new URLSearchParams({ limit: '100' })
if (after) params.set('after', after)
const response = await fetch(`https://api.hubapi.com/crm/v3/owners?${params.toString()}`, {
headers: { Authorization: `Bearer ${accessToken}` },
})

if (!response.ok) {
const errorText = await response.text().catch(() => '')
logger.error(`[${requestId}] HubSpot owners API error ${response.status}: ${errorText}`)
return NextResponse.json(
{ error: errorText || 'Failed to fetch HubSpot owners' },
{ status: response.status }
)
}

const data = (await response.json()) as {
results?: HubSpotOwner[]
paging?: { next?: { after?: string } }
}
if (data.results?.length) collected.push(...data.results)
after = data.paging?.next?.after
pages++
} while (after && pages < 10)

const filterTerm = (query as string | undefined)?.toLowerCase()
const owners = collected
.filter((o) => !o.archived)
.map((o) => ({
id: o.id,
name: [o.firstName, o.lastName].filter(Boolean).join(' ') || o.email || o.id,
email: o.email,
}))
.filter(
(o) =>
!filterTerm ||
o.name.toLowerCase().includes(filterTerm) ||
(o.email?.toLowerCase().includes(filterTerm) ?? false)
)
.sort((a, b) => a.name.localeCompare(b.name))

return NextResponse.json({ owners }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching HubSpot owners:`, error)
return NextResponse.json({ error: 'Failed to fetch HubSpot owners' }, { status: 500 })
}
})
83 changes: 83 additions & 0 deletions apps/sim/app/api/tools/hubspot/pipelines/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { hubspotPipelinesSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('HubSpotPipelinesAPI')

const BUILT_IN_PATH: Record<string, string> = {
contact: 'contacts',
company: 'companies',
deal: 'deals',
ticket: 'tickets',
}

interface HubSpotPipeline {
id: string
label: string
stages?: Array<{ id: string; label: string }>
archived?: boolean
}

export const GET = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()

try {
const parsed = await parseRequest(hubspotPipelinesSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credentialId, objectType } = parsed.data.query

const authz = await authorizeCredentialUse(request, {
credentialId,
requireWorkflowIdForInternal: false,
})
if (!authz.ok || !authz.credentialOwnerUserId || !authz.resolvedCredentialId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}

const pathSegment = BUILT_IN_PATH[objectType] ?? objectType
const response = await fetch(
`https://api.hubapi.com/crm/v3/pipelines/${encodeURIComponent(pathSegment)}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
)

if (!response.ok) {
const errorText = await response.text().catch(() => '')
logger.error(`[${requestId}] HubSpot pipelines API error ${response.status}: ${errorText}`)
return NextResponse.json(
{ error: errorText || 'Failed to fetch HubSpot pipelines' },
{ status: response.status }
)
}

const data = (await response.json()) as { results?: HubSpotPipeline[] }
const pipelines = (data.results ?? [])
.filter((p) => !p.archived)
.map((p) => ({
id: p.id,
name: p.label,
stages: p.stages?.map((s) => ({ id: s.id, label: s.label })),
}))
.sort((a, b) => a.name.localeCompare(b.name))

return NextResponse.json({ pipelines }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching HubSpot pipelines:`, error)
return NextResponse.json({ error: 'Failed to fetch HubSpot pipelines' }, { status: 500 })
}
})
99 changes: 99 additions & 0 deletions apps/sim/app/api/tools/hubspot/properties/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { hubspotPropertiesSelectorContract } from '@/lib/api/contracts/selectors/hubspot'
import { parseRequest } from '@/lib/api/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'

export const dynamic = 'force-dynamic'

const logger = createLogger('HubSpotPropertiesAPI')

const BUILT_IN_PATH: Record<string, string> = {
contact: 'contacts',
company: 'companies',
deal: 'deals',
ticket: 'tickets',
}

interface HubSpotProperty {
name: string
label: string
type?: string
fieldType?: string
groupName?: string
hidden?: boolean
archived?: boolean
}

export const GET = withRouteHandler(async (request: NextRequest) => {
const requestId = generateRequestId()

try {
const parsed = await parseRequest(hubspotPropertiesSelectorContract, request, {})
if (!parsed.success) return parsed.response
const { credentialId, objectType, query } = parsed.data.query

const authz = await authorizeCredentialUse(request, {
credentialId,
requireWorkflowIdForInternal: false,
})
if (!authz.ok || !authz.credentialOwnerUserId || !authz.resolvedCredentialId) {
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
}

const accessToken = await refreshAccessTokenIfNeeded(
credentialId,
authz.credentialOwnerUserId,
requestId
)
if (!accessToken) {
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
}

const pathSegment = BUILT_IN_PATH[objectType] ?? objectType
const response = await fetch(
`https://api.hubapi.com/crm/v3/properties/${encodeURIComponent(pathSegment)}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
)

if (!response.ok) {
const errorText = await response.text().catch(() => '')
logger.error(`[${requestId}] HubSpot properties API error ${response.status}: ${errorText}`)
return NextResponse.json(
{ error: errorText || 'Failed to fetch HubSpot properties' },
{ status: response.status }
)
}

const data = (await response.json()) as { results?: HubSpotProperty[] }
if (!Array.isArray(data.results)) {
return NextResponse.json({ error: 'Invalid HubSpot properties response' }, { status: 500 })
}

const filterTerm = (query as string | undefined)?.toLowerCase()
const properties = data.results
.filter((p) => !p.hidden && !p.archived)
.map((p) => ({
id: p.name,
name: p.label || p.name,
type: p.type,
fieldType: p.fieldType,
groupName: p.groupName,
}))
.filter(
(p) =>
!filterTerm ||
p.id.toLowerCase().includes(filterTerm) ||
p.name.toLowerCase().includes(filterTerm)
)
.sort((a, b) => a.name.localeCompare(b.name))

return NextResponse.json({ properties }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error fetching HubSpot properties:`, error)
return NextResponse.json({ error: 'Failed to fetch HubSpot properties' }, { status: 500 })
}
})
Loading
Loading