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
42 changes: 42 additions & 0 deletions apps/docs/content/docs/integrations/apollo.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,50 @@ import { ReferenceDetails } from "@/components/ui/reference-details"

{/* MANUAL-CONTENT-START:intro */}
Use [Apollo.io](https://apollo.io/) in Sim to search and enrich people and companies, manage CRM contacts and accounts, add contacts to sequences, and create follow-up tasks. Single-record and bulk enrichment operations are documented below.

Connect an Apollo account with OAuth, or save an API key as an Apollo credential. Existing API-key connections and workflows continue to work. OAuth requires an Apollo administrator or a user with permission to authorize third-party apps. Your Apollo plan, permissions, and credits still apply.

For self-hosting, register a public partner OAuth app in the [Apollo developer portal](https://developer.apollo.io/), configure `APOLLO_CLIENT_ID` and `APOLLO_CLIENT_SECRET`, and register `https://<your-domain>/api/auth/oauth2/callback/apollo` as a redirect URL. Enable the scopes listed below. API-key credentials remain available without an OAuth client.
{/* MANUAL-CONTENT-END */}

## OAuth scopes [#oauth-scopes]

Sim requests these scopes when someone connects an Apollo account. For a self-hosted deployment, register your own app with the provider using the settings below. See [Integrations and OAuth](/platform/self-hosting/integrations-oauth) for setup.

| Setting | Value |
| ------- | ----- |
| Redirect URI | `<NEXT_PUBLIC_APP_URL>/api/auth/oauth2/callback/apollo` |
| Environment variables | `APOLLO_CLIENT_ID`, `APOLLO_CLIENT_SECRET` |

| Scope | Description |
| ----- | ----------- |
| `read_user_profile` | View your Apollo profile |
| `mixed_people_api_search` | Search Apollo people |
| `people_match` | Enrich a person in Apollo |
| `people_bulk_match` | Enrich people in bulk in Apollo |
| `mixed_companies_search` | Search Apollo companies |
| `organizations_enrich` | Enrich an Apollo organization |
| `organizations_bulk_enrich` | Enrich organizations in bulk in Apollo |
| `contact_write` | Create Apollo contacts |
| `contact_update` | Update Apollo contacts |
| `contacts_search` | Search Apollo contacts |
| `contacts_bulk_create` | Create Apollo contacts in bulk |
| `contacts_bulk_update` | Update Apollo contacts in bulk |
| `account_write` | Create Apollo accounts |
| `account_update` | Update Apollo accounts |
| `accounts_search` | Search Apollo accounts |
| `account_bulk_create` | Create Apollo accounts in bulk |
| `account_stages_update` | Update Apollo account stages |
| `opportunity_write` | Create Apollo deals |
| `opportunities_list` | List Apollo deals |
| `opportunity_read` | View Apollo deals |
| `opportunity_update` | Update Apollo deals |
| `emailer_campaigns_search` | Search Apollo sequences |
| `emailer_campaigns_add_contact_ids` | Add contacts to Apollo sequences |
| `tasks_create` | Create Apollo tasks |
| `tasks_list` | Search Apollo tasks |
| `email_accounts_list` | View Apollo email accounts |

## Use the block [#usage-instructions]

Search for people and companies, enrich contact data, manage your CRM contacts and accounts, add contacts to sequences, and create tasks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ GitHub workflow blocks and knowledge-base token connections continue to use pers
| Help Scout | `HELPSCOUT_CLIENT_ID` / `HELPSCOUT_CLIENT_SECRET` | `helpscout` |
| HubSpot | `HUBSPOT_CLIENT_ID` / `HUBSPOT_CLIENT_SECRET` | `hubspot` |
| Salesforce | `SALESFORCE_CLIENT_ID` / `SALESFORCE_CLIENT_SECRET` | `salesforce` |
| Apollo | `APOLLO_CLIENT_ID` / `APOLLO_CLIENT_SECRET` | `apollo` |
| Pipedrive | `PIPEDRIVE_CLIENT_ID` / `PIPEDRIVE_CLIENT_SECRET` | `pipedrive` |
| Attio | `ATTIO_CLIENT_ID` / `ATTIO_CLIENT_SECRET` | `attio` |
| Zoho Desk | `ZOHO_CLIENT_ID` / `ZOHO_CLIENT_SECRET` | `zoho-desk` |
Expand Down
1 change: 1 addition & 0 deletions apps/docs/openapi-v2-resources.json
Original file line number Diff line number Diff line change
Expand Up @@ -18513,6 +18513,7 @@
"ramp",
"helpscout",
"hubspot",
"apollo",
"linkedin",
"instagram",
"salesforce",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,12 @@ import { useActiveSearchTarget } from '@/app/workspace/[workspaceId]/w/[workflow
import { BrandIcon } from '@/blocks/brand-icon'
import type { SubBlockConfig } from '@/blocks/types'
import { useWorkspaceCredential, useWorkspaceCredentials } from '@/hooks/queries/credentials'
import { useIntegrationAvailability } from '@/hooks/queries/integration-availability'
import { useOAuthCredentials } from '@/hooks/queries/oauth/credentials'
import { useCredentialRefreshTriggers } from '@/hooks/use-credential-refresh-triggers'
import { useIntegrationAccess } from '@/hooks/use-integration-access'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'

interface CredentialSelectorProps {
blockId: string
Expand All @@ -57,6 +60,7 @@ export function CredentialSelector({
const [editingValue, setEditingValue] = useState('')
const [isEditing, setIsEditing] = useState(false)
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
const blockType = useWorkflowStore((state) => state.blocks[blockId]?.type)
const [storeValue, setStoreValue] = useSubBlockValue<string | null>(blockId, subBlock.id)

const label = subBlock.placeholder || 'Select credential'
Expand Down Expand Up @@ -100,6 +104,20 @@ export function CredentialSelector({

const credentialKind = subBlock.credentialKind
const isMergedKinds = credentialKind === 'any'
const { getAccess } = useIntegrationAccess(workspaceId, {
enabled: !isAllCredentials && credentialKind !== 'service-account',
})
const { data: integrationAvailability, isSuccess: isAvailabilityReady } =
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
useIntegrationAvailability({
enabled: !isAllCredentials && credentialKind !== 'service-account',
})
const oauthConnectAvailable =
Boolean(blockType && getAccess(blockType) === 'available') &&
isAvailabilityReady &&
(integrationAvailability?.oauthServiceAvailability.some(
({ providerId, available }) => providerId === effectiveProviderId && available
) ??
false)

const credentials = useMemo(() => {
// A service-account picker lists only the reusable service-account
Expand Down Expand Up @@ -233,8 +251,9 @@ export function CredentialSelector({
setShowSetupModal(true)
return
}
if (!oauthConnectAvailable) return
setShowConnectModal(true)
}, [credentialKind])
}, [credentialKind, oauthConnectAvailable])

const getProviderIcon = useCallback((providerName: OAuthProvider) => {
const { baseProvider } = parseProvider(providerName)
Expand Down Expand Up @@ -278,7 +297,9 @@ export function CredentialSelector({
// Suppress the setup action when the service-account flow is preview-gated
// for this viewer (a custom Slack bot needs slack_v2) — existing accounts
// above stay selectable.
if (credentialKind !== 'service-account' || !serviceAccountConnectHidden) {
if (
credentialKind === 'service-account' ? !serviceAccountConnectHidden : oauthConnectAvailable
) {
options.push({
label:
credentialKind === 'service-account'
Expand All @@ -302,6 +323,7 @@ export function CredentialSelector({
credentialKind,
subBlock.credentialLabels,
serviceAccountConnectHidden,
oauthConnectAvailable,
serviceAccountTarget,
provider,
getProviderIcon,
Expand All @@ -323,11 +345,15 @@ export function CredentialSelector({
section: labels?.oauthGroup ?? `${getProviderName(provider)} accounts`,
items: [
...credentials.filter((c) => c.type !== 'service_account').map(toOption),
{
label: labels?.oauthConnect ?? `Connect ${getProviderName(provider)} account`,
value: '__connect_account__',
iconElement: <SquareArrowUpRight className='size-3' />,
},
...(oauthConnectAvailable
? [
{
label: labels?.oauthConnect ?? `Connect ${getProviderName(provider)} account`,
value: '__connect_account__',
iconElement: <SquareArrowUpRight className='size-3' />,
},
]
: []),
],
},
{
Expand Down Expand Up @@ -355,6 +381,7 @@ export function CredentialSelector({
subBlock.credentialLabels,
credentials,
serviceAccountConnectHidden,
oauthConnectAvailable,
serviceAccountTarget,
provider,
getProviderIcon,
Expand Down Expand Up @@ -407,10 +434,6 @@ export function CredentialSelector({
const handleComboboxChange = useCallback(
(value: string) => {
if (value === '__connect_account__') {
if (isMergedKinds) {
setShowConnectModal(true)
return
}
handleAddCredential()
return
}
Expand All @@ -432,14 +455,7 @@ export function CredentialSelector({
setIsEditing(true)
setEditingValue(value)
},
[
isAllCredentials,
isMergedKinds,
allWorkspaceCredentials,
credentials,
handleAddCredential,
handleSelect,
]
[isAllCredentials, allWorkspaceCredentials, credentials, handleAddCredential, handleSelect]
)

return (
Expand Down Expand Up @@ -470,31 +486,32 @@ export function CredentialSelector({
<span className='mr-1.5 inline-block size-[6px] rounded-xs bg-[var(--caution)]' />
{dataversePolicy.message}
</div>
{!dataversePolicy.hasInvalidEnvironment && (
<Chip
variant='primary'
fullWidth
onClick={() => {
if (dataversePolicy.requiresSeparateCredential) {
setShowConnectModal(true)
return
}
writeOAuthReturnContext({
origin: 'workflow',
workflowId: activeWorkflowId || '',
displayName: selectedCredential?.name ?? getProviderName(provider),
providerId: effectiveProviderId,
preCount: credentials.filter((c) => c.type !== 'service_account').length,
workspaceId,
reconnect: true,
requestedAt: Date.now(),
})
setShowOAuthModal(true)
}}
>
{dataversePolicy.actionLabel}
</Chip>
)}
{!dataversePolicy.hasInvalidEnvironment &&
(!dataversePolicy.requiresSeparateCredential || oauthConnectAvailable) && (
<Chip
variant='primary'
fullWidth
onClick={() => {
if (dataversePolicy.requiresSeparateCredential) {
setShowConnectModal(true)
return
}
writeOAuthReturnContext({
origin: 'workflow',
workflowId: activeWorkflowId || '',
displayName: selectedCredential?.name ?? getProviderName(provider),
providerId: effectiveProviderId,
preCount: credentials.filter((c) => c.type !== 'service_account').length,
workspaceId,
reconnect: true,
requestedAt: Date.now(),
})
setShowOAuthModal(true)
}}
>
{dataversePolicy.actionLabel}
</Chip>
)}
</div>
)}

Expand Down
1 change: 1 addition & 0 deletions apps/sim/blocks/blocks/apollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1600,5 +1600,6 @@ export const ApolloV2Block: BlockConfig = {
type: 'oauth-input',
serviceId: 'apollo',
title: 'Apollo Credential',
credentialKind: 'any',
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
}),
}
3 changes: 2 additions & 1 deletion apps/sim/blocks/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ interface SavedCredentialPicker {
type: 'oauth-input'
serviceId: SavedCredentialService
title: string
credentialKind?: 'service-account' | 'any'
}

interface SavedCredentialOptions {
Expand Down Expand Up @@ -160,7 +161,7 @@ export function withSavedCredential(
0,
{
...picker,
credentialKind: 'service-account',
credentialKind: picker.credentialKind ?? 'service-account',
canonicalParamId: 'oauthCredential',
mode: 'basic',
// 'AWS Credential' → 'Select AWS credential': sentence case, brand casing kept.
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/api/contracts/v2/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ export const V2_OAUTH_CONNECTION_PROVIDER_IDS = [
'ramp',
'helpscout',
'hubspot',
'apollo',
'linkedin',
'instagram',
'salesforce',
Expand Down
9 changes: 9 additions & 0 deletions apps/sim/lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { db } from '@sim/db'
import * as schema from '@sim/db/schema'
import { createLogger, setRequestAuth } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import { type BetterAuthOptions, betterAuth, type User } from 'better-auth'
import {
APIError,
Expand Down Expand Up @@ -159,6 +160,7 @@ import { validateSignupEmailMx } from '@/lib/messaging/email/validation.server'
import { isEmailVerificationEffectivelyEnabled } from '@/lib/messaging/email/verification'
import { scheduleLifecycleEmail } from '@/lib/messaging/lifecycle'
import { APP_ENTRY_PATH } from '@/lib/navigation/paths'
import { formatApolloAuthorizationUrl } from '@/lib/oauth/apollo'
import { clearRecordedRevocation } from '@/lib/oauth/credential-service'
import {
getMicrosoftRefreshTokenExpiry,
Expand Down Expand Up @@ -1242,6 +1244,13 @@ export const auth = betterAuth({
}
}),
after: createAuthMiddleware(async (ctx) => {
if (ctx.path === '/oauth2/link' && ctx.body?.providerId === 'apollo') {
const returned = ctx.context.returned
if (isRecordLike(returned) && typeof returned.url === 'string') {
ctx.context.returned = { ...returned, url: formatApolloAuthorizationUrl(returned.url) }
}
}

if (isBillingEnabled) await recordCustomerRestoreAfterHook(ctx)

const recordFinalAuthenticationResult = async (
Expand Down
8 changes: 8 additions & 0 deletions apps/sim/lib/auth/connectors/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
readResponseTextWithLimit,
} from '@/lib/core/utils/stream-limits'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createApolloProvider } from '@/lib/oauth/apollo'
import { exchangeAtlassianAuthorizationCode } from '@/lib/oauth/atlassian-token'
import { getDocusignOAuthUrl } from '@/lib/oauth/docusign'
import { createGitHubRepositoriesProvider } from '@/lib/oauth/github-repositories'
Expand Down Expand Up @@ -1042,6 +1043,13 @@ export function buildConnectorProviders(): GenericOAuthConfig[] {
},
},

createApolloProvider({
clientId: env.APOLLO_CLIENT_ID as string,
clientSecret: env.APOLLO_CLIENT_SECRET as string,
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/apollo`,
scopes: getCanonicalScopesForProvider('apollo'),
}),

{
providerId: 'pipedrive',
clientId: env.PIPEDRIVE_CLIENT_ID as string,
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,8 @@ export const env = createEnv({
ZOHO_CLIENT_SECRET: z.string().optional(), // Zoho OAuth client secret (Zoho Desk)
WEALTHBOX_CLIENT_ID: z.string().optional(), // WealthBox OAuth client ID
WEALTHBOX_CLIENT_SECRET: z.string().optional(), // WealthBox OAuth client secret
APOLLO_CLIENT_ID: z.string().optional(),
APOLLO_CLIENT_SECRET: z.string().optional(),
PIPEDRIVE_CLIENT_ID: z.string().optional(), // Pipedrive OAuth client ID
PIPEDRIVE_CLIENT_SECRET: z.string().optional(), // Pipedrive OAuth client secret
LINEAR_CLIENT_ID: z.string().optional(), // Linear OAuth client ID
Expand Down
Loading
Loading