diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index fcdab73224d..07c75bba9c1 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -2391,6 +2391,97 @@ export function FindymailIcon(props: SVGProps) { ) } +export function ZeroBounceIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + + + ) +} +export function NeverBounceIcon(props: SVGProps) { + return ( + + + + ) +} +export function MillionVerifierIcon(props: SVGProps) { + const id = useId() + const gradient = `millionverifier_${id}` + return ( + + + + + + + + + + + + ) +} export function FathomIcon(props: SVGProps) { return ( diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts index d22e1caf00f..db21891952a 100644 --- a/apps/docs/components/ui/icon-mapping.ts +++ b/apps/docs/components/ui/icon-mapping.ts @@ -125,11 +125,13 @@ import { MicrosoftPlannerIcon, MicrosoftSharepointIcon, MicrosoftTeamsIcon, + MillionVerifierIcon, MistralIcon, MondayIcon, MongoDBIcon, MySQLIcon, Neo4jIcon, + NeverBounceIcon, NewRelicIcon, NotionIcon, ObsidianIcon, @@ -210,6 +212,7 @@ import { YouTubeIcon, ZendeskIcon, ZepIcon, + ZeroBounceIcon, ZoomIcon, ZoomInfoIcon, } from '@/components/icons' @@ -354,6 +357,7 @@ export const blockTypeToIconMap: Record = { microsoft_excel_v2: MicrosoftExcelIcon, microsoft_planner: MicrosoftPlannerIcon, microsoft_teams: MicrosoftTeamsIcon, + millionverifier: MillionVerifierIcon, mistral_parse: MistralIcon, mistral_parse_v2: MistralIcon, mistral_parse_v3: MistralIcon, @@ -361,6 +365,7 @@ export const blockTypeToIconMap: Record = { mongodb: MongoDBIcon, mysql: MySQLIcon, neo4j: Neo4jIcon, + neverbounce: NeverBounceIcon, new_relic: NewRelicIcon, notion: NotionIcon, notion_v2: NotionIcon, @@ -453,6 +458,7 @@ export const blockTypeToIconMap: Record = { youtube: YouTubeIcon, zendesk: ZendeskIcon, zep: ZepIcon, + zerobounce: ZeroBounceIcon, zoom: ZoomIcon, zoominfo: ZoomInfoIcon, } diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index a17c92d28e7..32667164750 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -122,11 +122,13 @@ "microsoft_excel", "microsoft_planner", "microsoft_teams", + "millionverifier", "mistral_parse", "monday", "mongodb", "mysql", "neo4j", + "neverbounce", "new_relic", "notion", "obsidian", @@ -210,6 +212,7 @@ "youtube", "zendesk", "zep", + "zerobounce", "zoom", "zoominfo" ] diff --git a/apps/docs/content/docs/en/tools/millionverifier.mdx b/apps/docs/content/docs/en/tools/millionverifier.mdx new file mode 100644 index 00000000000..2c24baeeab2 --- /dev/null +++ b/apps/docs/content/docs/en/tools/millionverifier.mdx @@ -0,0 +1,65 @@ +--- +title: MillionVerifier +description: Verify email deliverability and check account credits +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +MillionVerifier is a high-volume, low-cost email verification service. Use this integration to verify an individual email in real time — it returns ok, catch-all, unknown, invalid, disposable, or unverified along with role-account and free-provider flags — and to check the verification credits remaining on your account. It's a cost-efficient choice for cleaning large lists before a campaign. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate MillionVerifier to verify email deliverability in real time — classify addresses as valid, invalid, catch-all, disposable, or unknown — and check your remaining verification credits. + + + +## Tools + +### `millionverifier_verify_email` + +Verify the deliverability of an email address. Uses one verification credit. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `email` | string | Yes | Email address to verify \(e.g., john@example.com\) | +| `apiKey` | string | Yes | MillionVerifier API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email` | string | The verified email address | +| `status` | string | Verification status \(valid, invalid, catch_all, disposable, unknown, unverified\) | +| `deliverable` | boolean | Whether the email is valid and safe to send | +| `freeEmail` | boolean | Whether the address is on a free email provider | +| `roleAccount` | boolean | Whether the address is a role account \(e.g., info@, sales@\) | +| `didYouMean` | string | Suggested correction for a likely typo | +| `subResult` | string | Additional MillionVerifier classification detail | + +### `millionverifier_get_credits` + +Retrieve the remaining verification credits for the authenticated account. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | MillionVerifier API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `credits` | number | Remaining verification credits | + + diff --git a/apps/docs/content/docs/en/tools/neverbounce.mdx b/apps/docs/content/docs/en/tools/neverbounce.mdx new file mode 100644 index 00000000000..7e546e9291a --- /dev/null +++ b/apps/docs/content/docs/en/tools/neverbounce.mdx @@ -0,0 +1,66 @@ +--- +title: NeverBounce +description: Verify email deliverability and check account credits +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +NeverBounce is a real-time email verification and list-cleaning service. Use this integration to check whether an email address is deliverable — it classifies each address as valid, invalid, disposable, catch-all, or unknown and surfaces role-account and free-provider flags — and to read the paid and free verification credits left on your account. Verify addresses before sending to cut bounces and keep your domain reputation healthy. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate NeverBounce to verify email deliverability in real time — classify addresses as valid, invalid, catch-all, disposable, or unknown — and check your remaining verification credits. + + + +## Tools + +### `neverbounce_verify_email` + +Verify the deliverability of an email address. Uses one verification credit. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `email` | string | Yes | Email address to verify \(e.g., john@example.com\) | +| `apiKey` | string | Yes | NeverBounce API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email` | string | The verified email address | +| `status` | string | Verification status \(valid, invalid, catch_all, disposable, unknown\) | +| `deliverable` | boolean | Whether the email is valid and safe to send | +| `roleAccount` | boolean | Whether the address is a role account \(e.g., info@, sales@\) | +| `freeEmail` | boolean | Whether the address is on a free email provider | +| `didYouMean` | string | Suggested correction for a likely typo | +| `flags` | array | Raw NeverBounce flags for the address | + +### `neverbounce_get_credits` + +Retrieve the remaining paid and free verification credits for the account. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | NeverBounce API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `credits` | number | Remaining paid verification credits | +| `freeCredits` | number | Remaining free verification credits | + + diff --git a/apps/docs/content/docs/en/tools/zerobounce.mdx b/apps/docs/content/docs/en/tools/zerobounce.mdx new file mode 100644 index 00000000000..e286db6656f --- /dev/null +++ b/apps/docs/content/docs/en/tools/zerobounce.mdx @@ -0,0 +1,64 @@ +--- +title: ZeroBounce +description: Validate email deliverability and check account credits +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +ZeroBounce is a real-time email validation and deliverability service. Use this integration to validate individual email addresses before outreach — it flags invalid, catch-all, spamtrap, abuse, and do-not-mail addresses so you can drop risky contacts and protect your sender reputation — and to check the validation credits remaining on your account. ZeroBounce is the default verifier behind many email-finding waterfalls. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrate ZeroBounce to validate email deliverability in real time — detect invalid, catch-all, spamtrap, abuse, and do-not-mail addresses — and check your remaining validation credits. + + + +## Tools + +### `zerobounce_verify_email` + +Validate an email address deliverability in real time. Uses one validation credit. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `email` | string | Yes | Email address to validate \(e.g., john@example.com\) | +| `apiKey` | string | Yes | ZeroBounce API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `email` | string | The validated email address | +| `status` | string | Validation status \(valid, invalid, catch_all, unknown, spamtrap, abuse, do_not_mail\) | +| `deliverable` | boolean | Whether the email is valid and safe to send | +| `subStatus` | string | Detailed sub-status from ZeroBounce | +| `freeEmail` | boolean | Whether the address is on a free email provider | +| `didYouMean` | string | Suggested correction for a likely typo | + +### `zerobounce_get_credits` + +Retrieve the remaining validation credits for the authenticated account. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | ZeroBounce API Key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `credits` | number | Remaining validation credits \(-1 if unavailable\) | + + diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index ef0582c6f41..8637615693b 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -124,11 +124,13 @@ import { MicrosoftPlannerIcon, MicrosoftSharepointIcon, MicrosoftTeamsIcon, + MillionVerifierIcon, MistralIcon, MondayIcon, MongoDBIcon, MySQLIcon, Neo4jIcon, + NeverBounceIcon, NewRelicIcon, NotionIcon, ObsidianIcon, @@ -209,6 +211,7 @@ import { YouTubeIcon, ZendeskIcon, ZepIcon, + ZeroBounceIcon, ZoomIcon, ZoomInfoIcon, } from '@/components/icons' @@ -336,11 +339,13 @@ export const blockTypeToIconMap: Record = { microsoft_excel_v2: MicrosoftExcelIcon, microsoft_planner: MicrosoftPlannerIcon, microsoft_teams: MicrosoftTeamsIcon, + millionverifier: MillionVerifierIcon, mistral_parse_v3: MistralIcon, monday: MondayIcon, mongodb: MongoDBIcon, mysql: MySQLIcon, neo4j: Neo4jIcon, + neverbounce: NeverBounceIcon, new_relic: NewRelicIcon, notion_v2: NotionIcon, obsidian: ObsidianIcon, @@ -423,6 +428,7 @@ export const blockTypeToIconMap: Record = { youtube: YouTubeIcon, zendesk: ZendeskIcon, zep: ZepIcon, + zerobounce: ZeroBounceIcon, zoom: ZoomIcon, zoominfo: ZoomInfoIcon, } diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index f371f03ebe5..40218482615 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -9640,6 +9640,33 @@ "integrationTypes": ["communication"], "tags": ["messaging", "microsoft-365"] }, + { + "type": "millionverifier", + "slug": "millionverifier", + "name": "MillionVerifier", + "description": "Verify email deliverability and check account credits", + "longDescription": "Integrate MillionVerifier to verify email deliverability in real time — classify addresses as valid, invalid, catch-all, disposable, or unknown — and check your remaining verification credits.", + "bgColor": "#00B67A", + "iconName": "MillionVerifierIcon", + "docsUrl": "https://docs.sim.ai/tools/millionverifier", + "operations": [ + { + "name": "Verify Email", + "description": "Verify the deliverability of an email address. Uses one verification credit." + }, + { + "name": "Get Remaining Credits", + "description": "Retrieve the remaining verification credits for the authenticated account." + } + ], + "operationCount": 2, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["sales"], + "tags": ["enrichment", "sales-engagement"] + }, { "type": "mistral_parse_v3", "slug": "mistral-parser", @@ -9908,6 +9935,33 @@ "integrationTypes": ["databases", "analytics"], "tags": ["data-warehouse", "data-analytics"] }, + { + "type": "neverbounce", + "slug": "neverbounce", + "name": "NeverBounce", + "description": "Verify email deliverability and check account credits", + "longDescription": "Integrate NeverBounce to verify email deliverability in real time — classify addresses as valid, invalid, catch-all, disposable, or unknown — and check your remaining verification credits.", + "bgColor": "#1A6DF0", + "iconName": "NeverBounceIcon", + "docsUrl": "https://docs.sim.ai/tools/neverbounce", + "operations": [ + { + "name": "Verify Email", + "description": "Verify the deliverability of an email address. Uses one verification credit." + }, + { + "name": "Get Remaining Credits", + "description": "Retrieve the remaining paid and free verification credits for the account." + } + ], + "operationCount": 2, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["sales"], + "tags": ["enrichment", "sales-engagement"] + }, { "type": "new_relic", "slug": "new-relic", @@ -15519,6 +15573,33 @@ "integrationTypes": ["ai", "documents", "search"], "tags": ["llm", "knowledge-base", "agentic"] }, + { + "type": "zerobounce", + "slug": "zerobounce", + "name": "ZeroBounce", + "description": "Validate email deliverability and check account credits", + "longDescription": "Integrate ZeroBounce to validate email deliverability in real time — detect invalid, catch-all, spamtrap, abuse, and do-not-mail addresses — and check your remaining validation credits.", + "bgColor": "#00B894", + "iconName": "ZeroBounceIcon", + "docsUrl": "https://docs.sim.ai/tools/zerobounce", + "operations": [ + { + "name": "Verify Email", + "description": "Validate an email address deliverability in real time. Uses one validation credit." + }, + { + "name": "Get Remaining Credits", + "description": "Retrieve the remaining validation credits for the authenticated account." + } + ], + "operationCount": 2, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools", + "integrationTypes": ["sales"], + "tags": ["enrichment", "sales-engagement"] + }, { "type": "zoom", "slug": "zoom", diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx index d289eea58dd..ca060e0d3ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/byok/byok.tsx @@ -29,7 +29,9 @@ import { ImageIcon, JinaAIIcon, LinkupIcon, + MillionVerifierIcon, MistralIcon, + NeverBounceIcon, OllamaIcon, OpenAIIcon, ParallelIcon, @@ -39,6 +41,7 @@ import { SerperIcon, TogetherIcon, WizaIcon, + ZeroBounceIcon, } from '@/components/icons' import { Input } from '@/components/ui' import { BYOKKeySkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton' @@ -220,6 +223,27 @@ const PROVIDERS: { description: 'Prospect search, individual reveal, and company enrichment', placeholder: 'Enter your Wiza API key', }, + { + id: 'zerobounce', + name: 'ZeroBounce', + icon: ZeroBounceIcon, + description: 'Real-time email validation and deliverability checks', + placeholder: 'Enter your ZeroBounce API key', + }, + { + id: 'neverbounce', + name: 'NeverBounce', + icon: NeverBounceIcon, + description: 'Real-time email verification and list cleaning', + placeholder: 'Enter your NeverBounce API key', + }, + { + id: 'millionverifier', + name: 'MillionVerifier', + icon: MillionVerifierIcon, + description: 'Real-time email verification and deliverability checks', + placeholder: 'Enter your MillionVerifier API key', + }, ] export function BYOK() { diff --git a/apps/sim/blocks/blocks/millionverifier.ts b/apps/sim/blocks/blocks/millionverifier.ts new file mode 100644 index 00000000000..830e818029b --- /dev/null +++ b/apps/sim/blocks/blocks/millionverifier.ts @@ -0,0 +1,102 @@ +import { MillionVerifierIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types' +import type { MillionVerifierResponse } from '@/tools/millionverifier/types' + +export const MillionVerifierBlock: BlockConfig = { + type: 'millionverifier', + name: 'MillionVerifier', + description: 'Verify email deliverability and check account credits', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate MillionVerifier to verify email deliverability in real time — classify addresses as valid, invalid, catch-all, disposable, or unknown — and check your remaining verification credits.', + docsLink: 'https://docs.sim.ai/tools/millionverifier', + category: 'tools', + integrationType: IntegrationType.Sales, + tags: ['enrichment', 'sales-engagement'], + bgColor: '#00B67A', + icon: MillionVerifierIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Verify Email', id: 'millionverifier_verify_email' }, + { label: 'Get Remaining Credits', id: 'millionverifier_get_credits' }, + ], + value: () => 'millionverifier_verify_email', + }, + { + id: 've_email', + title: 'Email Address', + type: 'short-input', + required: true, + placeholder: 'john@example.com', + condition: { field: 'operation', value: 'millionverifier_verify_email' }, + }, + // API Key — hidden on hosted Sim for operations with hosted-key support + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your MillionVerifier API key', + password: true, + hideWhenHosted: true, + condition: { field: 'operation', value: 'millionverifier_get_credits', not: true }, + }, + // API Key — always required for the credit-balance lookup (no hosted key) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your MillionVerifier API key', + password: true, + condition: { field: 'operation', value: 'millionverifier_get_credits' }, + }, + ], + tools: { + access: ['millionverifier_verify_email', 'millionverifier_get_credits'], + config: { + tool: (params) => { + switch (params.operation) { + case 'millionverifier_verify_email': + case 'millionverifier_get_credits': + return params.operation + default: + return 'millionverifier_verify_email' + } + }, + params: (params) => { + const { operation: _operation, ...rest } = params + const idToParam: Record = { ve_email: 'email' } + const result: Record = {} + for (const [key, value] of Object.entries(rest)) { + if (value === undefined || value === null || value === '') continue + const mappedKey = idToParam[key] ?? key + result[mappedKey] = value + } + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'MillionVerifier API key' }, + ve_email: { type: 'string', description: 'Email address to verify' }, + }, + outputs: { + email: { type: 'string', description: 'The verified email address' }, + status: { type: 'string', description: 'Verification status' }, + deliverable: { + type: 'boolean', + description: 'Whether the email is valid and safe to send', + }, + freeEmail: { type: 'boolean', description: 'Whether on a free email provider' }, + roleAccount: { type: 'boolean', description: 'Whether the address is a role account' }, + didYouMean: { type: 'string', description: 'Suggested correction' }, + subResult: { type: 'string', description: 'Additional classification detail' }, + credits: { type: 'number', description: 'Remaining verification credits' }, + }, +} diff --git a/apps/sim/blocks/blocks/neverbounce.ts b/apps/sim/blocks/blocks/neverbounce.ts new file mode 100644 index 00000000000..596724f3b56 --- /dev/null +++ b/apps/sim/blocks/blocks/neverbounce.ts @@ -0,0 +1,103 @@ +import { NeverBounceIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types' +import type { NeverBounceResponse } from '@/tools/neverbounce/types' + +export const NeverBounceBlock: BlockConfig = { + type: 'neverbounce', + name: 'NeverBounce', + description: 'Verify email deliverability and check account credits', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate NeverBounce to verify email deliverability in real time — classify addresses as valid, invalid, catch-all, disposable, or unknown — and check your remaining verification credits.', + docsLink: 'https://docs.sim.ai/tools/neverbounce', + category: 'tools', + integrationType: IntegrationType.Sales, + tags: ['enrichment', 'sales-engagement'], + bgColor: '#1A6DF0', + icon: NeverBounceIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Verify Email', id: 'neverbounce_verify_email' }, + { label: 'Get Remaining Credits', id: 'neverbounce_get_credits' }, + ], + value: () => 'neverbounce_verify_email', + }, + { + id: 've_email', + title: 'Email Address', + type: 'short-input', + required: true, + placeholder: 'john@example.com', + condition: { field: 'operation', value: 'neverbounce_verify_email' }, + }, + // API Key — hidden on hosted Sim for operations with hosted-key support + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your NeverBounce API key', + password: true, + hideWhenHosted: true, + condition: { field: 'operation', value: 'neverbounce_get_credits', not: true }, + }, + // API Key — always required for the credit-balance lookup (no hosted key) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your NeverBounce API key', + password: true, + condition: { field: 'operation', value: 'neverbounce_get_credits' }, + }, + ], + tools: { + access: ['neverbounce_verify_email', 'neverbounce_get_credits'], + config: { + tool: (params) => { + switch (params.operation) { + case 'neverbounce_verify_email': + case 'neverbounce_get_credits': + return params.operation + default: + return 'neverbounce_verify_email' + } + }, + params: (params) => { + const { operation: _operation, ...rest } = params + const idToParam: Record = { ve_email: 'email' } + const result: Record = {} + for (const [key, value] of Object.entries(rest)) { + if (value === undefined || value === null || value === '') continue + const mappedKey = idToParam[key] ?? key + result[mappedKey] = value + } + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'NeverBounce API key' }, + ve_email: { type: 'string', description: 'Email address to verify' }, + }, + outputs: { + email: { type: 'string', description: 'The verified email address' }, + status: { type: 'string', description: 'Verification status' }, + deliverable: { + type: 'boolean', + description: 'Whether the email is valid and safe to send', + }, + roleAccount: { type: 'boolean', description: 'Whether the address is a role account' }, + freeEmail: { type: 'boolean', description: 'Whether on a free email provider' }, + didYouMean: { type: 'string', description: 'Suggested correction' }, + flags: { type: 'array', description: 'Raw NeverBounce flags' }, + credits: { type: 'number', description: 'Remaining paid verification credits' }, + freeCredits: { type: 'number', description: 'Remaining free verification credits' }, + }, +} diff --git a/apps/sim/blocks/blocks/zerobounce.ts b/apps/sim/blocks/blocks/zerobounce.ts new file mode 100644 index 00000000000..39113c70cf3 --- /dev/null +++ b/apps/sim/blocks/blocks/zerobounce.ts @@ -0,0 +1,101 @@ +import { ZeroBounceIcon } from '@/components/icons' +import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types' +import type { ZeroBounceResponse } from '@/tools/zerobounce/types' + +export const ZeroBounceBlock: BlockConfig = { + type: 'zerobounce', + name: 'ZeroBounce', + description: 'Validate email deliverability and check account credits', + authMode: AuthMode.ApiKey, + longDescription: + 'Integrate ZeroBounce to validate email deliverability in real time — detect invalid, catch-all, spamtrap, abuse, and do-not-mail addresses — and check your remaining validation credits.', + docsLink: 'https://docs.sim.ai/tools/zerobounce', + category: 'tools', + integrationType: IntegrationType.Sales, + tags: ['enrichment', 'sales-engagement'], + bgColor: '#00B894', + icon: ZeroBounceIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Verify Email', id: 'zerobounce_verify_email' }, + { label: 'Get Remaining Credits', id: 'zerobounce_get_credits' }, + ], + value: () => 'zerobounce_verify_email', + }, + { + id: 've_email', + title: 'Email Address', + type: 'short-input', + required: true, + placeholder: 'john@example.com', + condition: { field: 'operation', value: 'zerobounce_verify_email' }, + }, + // API Key — hidden on hosted Sim for operations with hosted-key support + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your ZeroBounce API key', + password: true, + hideWhenHosted: true, + condition: { field: 'operation', value: 'zerobounce_get_credits', not: true }, + }, + // API Key — always required for the credit-balance lookup (no hosted key) + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + required: true, + placeholder: 'Enter your ZeroBounce API key', + password: true, + condition: { field: 'operation', value: 'zerobounce_get_credits' }, + }, + ], + tools: { + access: ['zerobounce_verify_email', 'zerobounce_get_credits'], + config: { + tool: (params) => { + switch (params.operation) { + case 'zerobounce_verify_email': + case 'zerobounce_get_credits': + return params.operation + default: + return 'zerobounce_verify_email' + } + }, + params: (params) => { + const { operation: _operation, ...rest } = params + const idToParam: Record = { ve_email: 'email' } + const result: Record = {} + for (const [key, value] of Object.entries(rest)) { + if (value === undefined || value === null || value === '') continue + const mappedKey = idToParam[key] ?? key + result[mappedKey] = value + } + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + apiKey: { type: 'string', description: 'ZeroBounce API key' }, + ve_email: { type: 'string', description: 'Email address to validate' }, + }, + outputs: { + email: { type: 'string', description: 'The validated email address' }, + status: { type: 'string', description: 'Validation status' }, + deliverable: { + type: 'boolean', + description: 'Whether the email is valid and safe to send', + }, + subStatus: { type: 'string', description: 'Detailed sub-status' }, + freeEmail: { type: 'boolean', description: 'Whether on a free email provider' }, + didYouMean: { type: 'string', description: 'Suggested correction' }, + credits: { type: 'number', description: 'Remaining validation credits' }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index d13aa5d2bcc..77d570a1d4e 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -133,6 +133,7 @@ import { MicrosoftDataverseBlock } from '@/blocks/blocks/microsoft_dataverse' import { MicrosoftExcelBlock, MicrosoftExcelV2Block } from '@/blocks/blocks/microsoft_excel' import { MicrosoftPlannerBlock } from '@/blocks/blocks/microsoft_planner' import { MicrosoftTeamsBlock } from '@/blocks/blocks/microsoft_teams' +import { MillionVerifierBlock } from '@/blocks/blocks/millionverifier' import { MistralParseBlock, MistralParseV2Block, @@ -143,6 +144,7 @@ import { MongoDBBlock } from '@/blocks/blocks/mongodb' import { MothershipBlock } from '@/blocks/blocks/mothership' import { MySQLBlock } from '@/blocks/blocks/mysql' import { Neo4jBlock } from '@/blocks/blocks/neo4j' +import { NeverBounceBlock } from '@/blocks/blocks/neverbounce' import { NewRelicBlock } from '@/blocks/blocks/new_relic' import { NoteBlock } from '@/blocks/blocks/note' import { NotionBlock, NotionV2Block } from '@/blocks/blocks/notion' @@ -245,6 +247,7 @@ import { XBlock } from '@/blocks/blocks/x' import { YouTubeBlock } from '@/blocks/blocks/youtube' import { ZendeskBlock } from '@/blocks/blocks/zendesk' import { ZepBlock } from '@/blocks/blocks/zep' +import { ZeroBounceBlock } from '@/blocks/blocks/zerobounce' import { ZoomBlock } from '@/blocks/blocks/zoom' import { ZoomInfoBlock } from '@/blocks/blocks/zoominfo' import type { BlockConfig } from '@/blocks/types' @@ -318,6 +321,9 @@ export const registry: Record = { file_v3: FileV3Block, file_v4: FileV4Block, findymail: FindymailBlock, + zerobounce: ZeroBounceBlock, + neverbounce: NeverBounceBlock, + millionverifier: MillionVerifierBlock, firecrawl: FirecrawlBlock, fireflies: FirefliesBlock, fireflies_v2: FirefliesV2Block, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index fcdab73224d..07c75bba9c1 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2391,6 +2391,97 @@ export function FindymailIcon(props: SVGProps) { ) } +export function ZeroBounceIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + + + ) +} +export function NeverBounceIcon(props: SVGProps) { + return ( + + + + ) +} +export function MillionVerifierIcon(props: SVGProps) { + const id = useId() + const gradient = `millionverifier_${id}` + return ( + + + + + + + + + + + + ) +} export function FathomIcon(props: SVGProps) { return ( diff --git a/apps/sim/enrichments/email-verification/email-verification.ts b/apps/sim/enrichments/email-verification/email-verification.ts new file mode 100644 index 00000000000..71cb3f9b321 --- /dev/null +++ b/apps/sim/enrichments/email-verification/email-verification.ts @@ -0,0 +1,71 @@ +import { ShieldCheck } from '@/components/emcn/icons' +import { str, toolProvider } from '@/enrichments/providers' +import type { EnrichmentConfig } from '@/enrichments/types' + +/** + * Email Verification enrichment. Checks an email address's deliverability via a + * verifier waterfall — ZeroBounce first (highest coverage), then NeverBounce, + * then MillionVerifier. A provider that returns a definitive verdict + * (valid / invalid / catch_all / disposable / etc.) fills the cell; a provider + * that can only return `unknown` falls through to the next so the row gets the + * most confident answer available. All providers support hosted keys. + */ +export const emailVerificationEnrichment: EnrichmentConfig = { + id: 'email-verification', + name: 'Email Verification', + description: "Check an email address's deliverability and risk status.", + icon: ShieldCheck, + inputs: [{ id: 'email', name: 'Email', type: 'string', required: true }], + outputs: [ + { id: 'status', name: 'status', type: 'string' }, + { id: 'deliverable', name: 'deliverable', type: 'boolean' }, + ], + providers: [ + toolProvider({ + id: 'zerobounce', + label: 'ZeroBounce', + toolId: 'zerobounce_verify_email', + buildParams: (inputs) => { + const email = str(inputs.email) + if (!email) return null + return { email } + }, + mapOutput: (output) => { + const status = str(output.status) + // Fall through to the next verifier when the verdict is missing or inconclusive. + if (!status || status === 'unknown') return null + return { status, deliverable: output.deliverable === true } + }, + }), + toolProvider({ + id: 'neverbounce', + label: 'NeverBounce', + toolId: 'neverbounce_verify_email', + buildParams: (inputs) => { + const email = str(inputs.email) + if (!email) return null + return { email } + }, + mapOutput: (output) => { + const status = str(output.status) + if (!status || status === 'unknown') return null + return { status, deliverable: output.deliverable === true } + }, + }), + toolProvider({ + id: 'millionverifier', + label: 'MillionVerifier', + toolId: 'millionverifier_verify_email', + buildParams: (inputs) => { + const email = str(inputs.email) + if (!email) return null + return { email } + }, + mapOutput: (output) => { + const status = str(output.status) + if (!status || status === 'unknown') return null + return { status, deliverable: output.deliverable === true } + }, + }), + ], +} diff --git a/apps/sim/enrichments/email-verification/index.ts b/apps/sim/enrichments/email-verification/index.ts new file mode 100644 index 00000000000..0d742adc57c --- /dev/null +++ b/apps/sim/enrichments/email-verification/index.ts @@ -0,0 +1 @@ +export { emailVerificationEnrichment } from './email-verification' diff --git a/apps/sim/enrichments/registry.ts b/apps/sim/enrichments/registry.ts index dce9c36fb77..daeb131b3d9 100644 --- a/apps/sim/enrichments/registry.ts +++ b/apps/sim/enrichments/registry.ts @@ -1,11 +1,13 @@ import { companyDomainEnrichment } from '@/enrichments/company-domain' import { companyInfoEnrichment } from '@/enrichments/company-info' +import { emailVerificationEnrichment } from '@/enrichments/email-verification' import { phoneNumberEnrichment } from '@/enrichments/phone-number' import type { EnrichmentConfig, EnrichmentRegistry } from '@/enrichments/types' import { workEmailEnrichment } from '@/enrichments/work-email' export const ENRICHMENT_REGISTRY: EnrichmentRegistry = { [workEmailEnrichment.id]: workEmailEnrichment, + [emailVerificationEnrichment.id]: emailVerificationEnrichment, [phoneNumberEnrichment.id]: phoneNumberEnrichment, [companyDomainEnrichment.id]: companyDomainEnrichment, [companyInfoEnrichment.id]: companyInfoEnrichment, diff --git a/apps/sim/lib/api/contracts/byok-keys.ts b/apps/sim/lib/api/contracts/byok-keys.ts index eb160128997..7b66d89ee71 100644 --- a/apps/sim/lib/api/contracts/byok-keys.ts +++ b/apps/sim/lib/api/contracts/byok-keys.ts @@ -26,6 +26,9 @@ export const byokProviderIdSchema = z.enum([ 'findymail', 'prospeo', 'wiza', + 'zerobounce', + 'neverbounce', + 'millionverifier', ]) export const byokKeySchema = z.object({ diff --git a/apps/sim/tools/millionverifier/get_credits.ts b/apps/sim/tools/millionverifier/get_credits.ts new file mode 100644 index 00000000000..bd1cffc822f --- /dev/null +++ b/apps/sim/tools/millionverifier/get_credits.ts @@ -0,0 +1,56 @@ +import type { + MillionVerifierGetCreditsParams, + MillionVerifierGetCreditsResponse, +} from '@/tools/millionverifier/types' +import type { ToolConfig } from '@/tools/types' + +export const getCreditsTool: ToolConfig< + MillionVerifierGetCreditsParams, + MillionVerifierGetCreditsResponse +> = { + id: 'millionverifier_get_credits', + name: 'MillionVerifier Get Credits', + description: 'Retrieve the remaining verification credits for the authenticated account.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'MillionVerifier API Key', + }, + }, + + request: { + url: (params) => + `https://api.millionverifier.com/api/v3/credits?api=${encodeURIComponent(params.apiKey.trim())}`, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json().catch(() => ({})) + const errorMessage = + typeof data === 'object' && data !== null && typeof data.error === 'string' ? data.error : '' + if (!response.ok || errorMessage.length > 0) { + return { + success: false, + error: + errorMessage || `MillionVerifier API error: ${response.status} ${response.statusText}`, + output: { credits: 0 }, + } + } + // The credits endpoint may return either `{ credits }` or a bare number. + const raw = typeof data === 'number' ? data : (data.credits ?? 0) + const credits = Number(raw) + return { + success: true, + output: { credits: Number.isNaN(credits) ? 0 : credits }, + } + }, + + outputs: { + credits: { type: 'number', description: 'Remaining verification credits' }, + }, +} diff --git a/apps/sim/tools/millionverifier/hosting.ts b/apps/sim/tools/millionverifier/hosting.ts new file mode 100644 index 00000000000..fa0c4cda4e2 --- /dev/null +++ b/apps/sim/tools/millionverifier/hosting.ts @@ -0,0 +1,39 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for MillionVerifier hosted keys. Provide keys as + * `MILLIONVERIFIER_API_KEY_COUNT` plus `MILLIONVERIFIER_API_KEY_1..N`. + */ +export const MILLIONVERIFIER_API_KEY_PREFIX = 'MILLIONVERIFIER_API_KEY' + +/** + * Dollar cost of a single MillionVerifier verification credit. MillionVerifier + * charges one credit per email checked; estimated from the bulk credit plans + * (≈ $0.0012/credit at volume) — https://millionverifier.com/pricing. + */ +export const MILLIONVERIFIER_CREDIT_USD = 0.0012 + +/** + * Build a MillionVerifier `hosting` config. `getCredits` returns the number of + * verification credits the call consumed (one per checked email). + */ +export function millionverifierHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: MILLIONVERIFIER_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'millionverifier', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * MILLIONVERIFIER_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/millionverifier/index.ts b/apps/sim/tools/millionverifier/index.ts new file mode 100644 index 00000000000..e146b71f845 --- /dev/null +++ b/apps/sim/tools/millionverifier/index.ts @@ -0,0 +1,7 @@ +export * from './types' + +import { getCreditsTool } from '@/tools/millionverifier/get_credits' +import { verifyEmailTool } from '@/tools/millionverifier/verify_email' + +export const millionverifierVerifyEmailTool = verifyEmailTool +export const millionverifierGetCreditsTool = getCreditsTool diff --git a/apps/sim/tools/millionverifier/types.ts b/apps/sim/tools/millionverifier/types.ts new file mode 100644 index 00000000000..1e3dbda9c02 --- /dev/null +++ b/apps/sim/tools/millionverifier/types.ts @@ -0,0 +1,32 @@ +import type { ToolResponse } from '@/tools/types' + +export interface MillionVerifierVerifyEmailParams { + email: string + apiKey: string +} + +export interface MillionVerifierVerifyEmailResponse extends ToolResponse { + output: { + email: string + status: string + deliverable: boolean + freeEmail?: boolean + roleAccount?: boolean + didYouMean?: string + subResult?: string + } +} + +export interface MillionVerifierGetCreditsParams { + apiKey: string +} + +export interface MillionVerifierGetCreditsResponse extends ToolResponse { + output: { + credits: number + } +} + +export type MillionVerifierResponse = + | MillionVerifierVerifyEmailResponse + | MillionVerifierGetCreditsResponse diff --git a/apps/sim/tools/millionverifier/verify_email.ts b/apps/sim/tools/millionverifier/verify_email.ts new file mode 100644 index 00000000000..a8ea5a936c6 --- /dev/null +++ b/apps/sim/tools/millionverifier/verify_email.ts @@ -0,0 +1,115 @@ +import { millionverifierHosting } from '@/tools/millionverifier/hosting' +import type { + MillionVerifierVerifyEmailParams, + MillionVerifierVerifyEmailResponse, +} from '@/tools/millionverifier/types' +import type { ToolConfig } from '@/tools/types' + +/** Maps a MillionVerifier `result` to the shared verification vocabulary. */ +const STATUS_MAP: Record = { + ok: 'valid', + invalid: 'invalid', + catch_all: 'catch_all', + disposable: 'disposable', + unknown: 'unknown', + unverified: 'unverified', +} + +export const verifyEmailTool: ToolConfig< + MillionVerifierVerifyEmailParams, + MillionVerifierVerifyEmailResponse +> = { + id: 'millionverifier_verify_email', + name: 'MillionVerifier Verify Email', + description: 'Verify the deliverability of an email address. Uses one verification credit.', + version: '1.0.0', + + hosting: millionverifierHosting(() => { + // Each verification consumes one MillionVerifier credit. + return 1 + }), + + params: { + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address to verify (e.g., john@example.com)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'MillionVerifier API Key', + }, + }, + + request: { + url: (params) => + `https://api.millionverifier.com/api/v3/?api=${encodeURIComponent( + params.apiKey.trim() + )}&email=${encodeURIComponent(params.email.trim())}&timeout=10`, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json().catch(() => ({})) + const errorMessage = + typeof data === 'object' && data !== null && typeof data.error === 'string' ? data.error : '' + if (!response.ok || errorMessage.length > 0) { + return { + success: false, + error: + errorMessage || `MillionVerifier API error: ${response.status} ${response.statusText}`, + output: { email: '', status: '', deliverable: false }, + } + } + const result = String(data.result ?? '') + return { + success: true, + output: { + email: data.email ?? '', + status: STATUS_MAP[result] ?? result, + deliverable: result === 'ok', + freeEmail: data.free ?? false, + roleAccount: data.role ?? false, + didYouMean: data.didyoumean ?? '', + subResult: data.subresult ?? '', + }, + } + }, + + outputs: { + email: { type: 'string', description: 'The verified email address' }, + status: { + type: 'string', + description: + 'Verification status (valid, invalid, catch_all, disposable, unknown, unverified)', + }, + deliverable: { + type: 'boolean', + description: 'Whether the email is valid and safe to send', + }, + freeEmail: { + type: 'boolean', + description: 'Whether the address is on a free email provider', + optional: true, + }, + roleAccount: { + type: 'boolean', + description: 'Whether the address is a role account (e.g., info@, sales@)', + optional: true, + }, + didYouMean: { + type: 'string', + description: 'Suggested correction for a likely typo', + optional: true, + }, + subResult: { + type: 'string', + description: 'Additional MillionVerifier classification detail', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/neverbounce/get_credits.ts b/apps/sim/tools/neverbounce/get_credits.ts new file mode 100644 index 00000000000..84485d4181a --- /dev/null +++ b/apps/sim/tools/neverbounce/get_credits.ts @@ -0,0 +1,57 @@ +import type { + NeverBounceGetCreditsParams, + NeverBounceGetCreditsResponse, +} from '@/tools/neverbounce/types' +import type { ToolConfig } from '@/tools/types' + +export const getCreditsTool: ToolConfig< + NeverBounceGetCreditsParams, + NeverBounceGetCreditsResponse +> = { + id: 'neverbounce_get_credits', + name: 'NeverBounce Get Credits', + description: 'Retrieve the remaining paid and free verification credits for the account.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'NeverBounce API Key', + }, + }, + + request: { + url: (params) => + `https://api.neverbounce.com/v4/account/info?key=${encodeURIComponent(params.apiKey.trim())}`, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json().catch(() => ({})) + if (!response.ok || data.status !== 'success') { + return { + success: false, + error: + (data as Record).message || + `NeverBounce API error: ${response.status} ${response.statusText}`, + output: { credits: 0, freeCredits: 0 }, + } + } + const creditsInfo = (data.credits_info ?? {}) as Record + return { + success: true, + output: { + credits: creditsInfo.paid_credits_remaining ?? 0, + freeCredits: creditsInfo.free_credits_remaining ?? 0, + }, + } + }, + + outputs: { + credits: { type: 'number', description: 'Remaining paid verification credits' }, + freeCredits: { type: 'number', description: 'Remaining free verification credits' }, + }, +} diff --git a/apps/sim/tools/neverbounce/hosting.ts b/apps/sim/tools/neverbounce/hosting.ts new file mode 100644 index 00000000000..0406ca5cadb --- /dev/null +++ b/apps/sim/tools/neverbounce/hosting.ts @@ -0,0 +1,39 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for NeverBounce hosted keys. Provide keys as + * `NEVERBOUNCE_API_KEY_COUNT` plus `NEVERBOUNCE_API_KEY_1..N`. + */ +export const NEVERBOUNCE_API_KEY_PREFIX = 'NEVERBOUNCE_API_KEY' + +/** + * Dollar cost of a single NeverBounce verification credit. NeverBounce charges + * one credit per email checked; estimated from the pay-as-you-go tiers and + * rounded up for smaller plans — https://neverbounce.com/pricing. + */ +export const NEVERBOUNCE_CREDIT_USD = 0.008 + +/** + * Build a NeverBounce `hosting` config. `getCredits` returns the number of + * verification credits the call consumed (one per checked email). + */ +export function neverbounceHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: NEVERBOUNCE_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'neverbounce', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * NEVERBOUNCE_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/neverbounce/index.ts b/apps/sim/tools/neverbounce/index.ts new file mode 100644 index 00000000000..dd67c64a74c --- /dev/null +++ b/apps/sim/tools/neverbounce/index.ts @@ -0,0 +1,7 @@ +export * from './types' + +import { getCreditsTool } from '@/tools/neverbounce/get_credits' +import { verifyEmailTool } from '@/tools/neverbounce/verify_email' + +export const neverbounceVerifyEmailTool = verifyEmailTool +export const neverbounceGetCreditsTool = getCreditsTool diff --git a/apps/sim/tools/neverbounce/types.ts b/apps/sim/tools/neverbounce/types.ts new file mode 100644 index 00000000000..a57caa6bd46 --- /dev/null +++ b/apps/sim/tools/neverbounce/types.ts @@ -0,0 +1,31 @@ +import type { ToolResponse } from '@/tools/types' + +export interface NeverBounceVerifyEmailParams { + email: string + apiKey: string +} + +export interface NeverBounceVerifyEmailResponse extends ToolResponse { + output: { + email: string + status: string + deliverable: boolean + roleAccount?: boolean + freeEmail?: boolean + didYouMean?: string + flags?: string[] + } +} + +export interface NeverBounceGetCreditsParams { + apiKey: string +} + +export interface NeverBounceGetCreditsResponse extends ToolResponse { + output: { + credits: number + freeCredits: number + } +} + +export type NeverBounceResponse = NeverBounceVerifyEmailResponse | NeverBounceGetCreditsResponse diff --git a/apps/sim/tools/neverbounce/verify_email.ts b/apps/sim/tools/neverbounce/verify_email.ts new file mode 100644 index 00000000000..b8c5e600bf0 --- /dev/null +++ b/apps/sim/tools/neverbounce/verify_email.ts @@ -0,0 +1,115 @@ +import { neverbounceHosting } from '@/tools/neverbounce/hosting' +import type { + NeverBounceVerifyEmailParams, + NeverBounceVerifyEmailResponse, +} from '@/tools/neverbounce/types' +import type { ToolConfig } from '@/tools/types' + +/** Maps a NeverBounce `result` to the shared verification vocabulary. */ +const STATUS_MAP: Record = { + valid: 'valid', + invalid: 'invalid', + catchall: 'catch_all', + disposable: 'disposable', + unknown: 'unknown', +} + +export const verifyEmailTool: ToolConfig< + NeverBounceVerifyEmailParams, + NeverBounceVerifyEmailResponse +> = { + id: 'neverbounce_verify_email', + name: 'NeverBounce Verify Email', + description: 'Verify the deliverability of an email address. Uses one verification credit.', + version: '1.0.0', + + hosting: neverbounceHosting(() => { + // Each verification consumes one NeverBounce credit. + return 1 + }), + + params: { + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address to verify (e.g., john@example.com)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'NeverBounce API Key', + }, + }, + + request: { + url: (params) => + `https://api.neverbounce.com/v4/single/check?key=${encodeURIComponent( + params.apiKey.trim() + )}&email=${encodeURIComponent(params.email.trim())}&address_info=1`, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response, params?: NeverBounceVerifyEmailParams) => { + const data = await response.json().catch(() => ({})) + // NeverBounce returns HTTP 200 for API-level errors; the envelope status + // distinguishes a successful check from an auth/quota failure. + if (!response.ok || data.status !== 'success') { + return { + success: false, + error: + (data as Record).message || + `NeverBounce API error: ${response.status} ${response.statusText}`, + output: { email: params?.email ?? '', status: '', deliverable: false }, + } + } + const result = String(data.result ?? '') + const flags: string[] = Array.isArray(data.flags) ? data.flags : [] + return { + success: true, + output: { + email: params?.email ?? '', + status: STATUS_MAP[result] ?? result, + deliverable: result === 'valid', + roleAccount: flags.includes('role_account'), + freeEmail: flags.includes('free_email_host'), + didYouMean: data.suggested_correction ?? '', + flags, + }, + } + }, + + outputs: { + email: { type: 'string', description: 'The verified email address' }, + status: { + type: 'string', + description: 'Verification status (valid, invalid, catch_all, disposable, unknown)', + }, + deliverable: { + type: 'boolean', + description: 'Whether the email is valid and safe to send', + }, + roleAccount: { + type: 'boolean', + description: 'Whether the address is a role account (e.g., info@, sales@)', + optional: true, + }, + freeEmail: { + type: 'boolean', + description: 'Whether the address is on a free email provider', + optional: true, + }, + didYouMean: { + type: 'string', + description: 'Suggested correction for a likely typo', + optional: true, + }, + flags: { + type: 'array', + description: 'Raw NeverBounce flags for the address', + optional: true, + }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index ff6a5ce0b86..efa5de16a4c 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1913,6 +1913,10 @@ import { microsoftTeamsWriteChannelTool, microsoftTeamsWriteChatTool, } from '@/tools/microsoft_teams' +import { + millionverifierGetCreditsTool, + millionverifierVerifyEmailTool, +} from '@/tools/millionverifier' import { mistralParserTool, mistralParserV2Tool, mistralParserV3Tool } from '@/tools/mistral' import { mondayArchiveItemTool, @@ -1954,6 +1958,7 @@ import { neo4jQueryTool, neo4jUpdateTool, } from '@/tools/neo4j' +import { neverbounceGetCreditsTool, neverbounceVerifyEmailTool } from '@/tools/neverbounce' import { newRelicCreateDeploymentEventTool, newRelicGetEntityTool, @@ -3236,6 +3241,7 @@ import { zepGetUserThreadsTool, zepGetUserTool, } from '@/tools/zep' +import { zerobounceGetCreditsTool, zerobounceVerifyEmailTool } from '@/tools/zerobounce' import { zoomCreateMeetingTool, zoomDeleteMeetingTool, @@ -5156,6 +5162,12 @@ export const tools: Record = { findymail_reverse_email_lookup: findymailReverseEmailLookupTool, findymail_search_technologies: findymailSearchTechnologiesTool, findymail_verify_email: findymailVerifyEmailTool, + zerobounce_verify_email: zerobounceVerifyEmailTool, + zerobounce_get_credits: zerobounceGetCreditsTool, + neverbounce_verify_email: neverbounceVerifyEmailTool, + neverbounce_get_credits: neverbounceGetCreditsTool, + millionverifier_verify_email: millionverifierVerifyEmailTool, + millionverifier_get_credits: millionverifierGetCreditsTool, stt_whisper: whisperSttTool, stt_whisper_v2: whisperSttV2Tool, stt_deepgram: deepgramSttTool, diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index fad0f65c80c..c8da61e06cc 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -27,6 +27,9 @@ export type BYOKProviderId = | 'findymail' | 'prospeo' | 'wiza' + | 'zerobounce' + | 'neverbounce' + | 'millionverifier' export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' diff --git a/apps/sim/tools/zerobounce/get_credits.ts b/apps/sim/tools/zerobounce/get_credits.ts new file mode 100644 index 00000000000..06de985ad63 --- /dev/null +++ b/apps/sim/tools/zerobounce/get_credits.ts @@ -0,0 +1,58 @@ +import type { ToolConfig } from '@/tools/types' +import type { + ZeroBounceGetCreditsParams, + ZeroBounceGetCreditsResponse, +} from '@/tools/zerobounce/types' + +export const getCreditsTool: ToolConfig = + { + id: 'zerobounce_get_credits', + name: 'ZeroBounce Get Credits', + description: 'Retrieve the remaining validation credits for the authenticated account.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZeroBounce API Key', + }, + }, + + request: { + url: (params) => + `https://api.zerobounce.net/v2/getcredits?api_key=${encodeURIComponent(params.apiKey.trim())}`, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json().catch(() => ({})) + // ZeroBounce returns HTTP 200 with an `{ error }` envelope on auth failure, + // so detect API-level errors from the body, not just the HTTP status. + const errorMessage = + typeof data === 'object' && data !== null && typeof data.error === 'string' + ? data.error + : '' + if (!response.ok || errorMessage.length > 0) { + return { + success: false, + error: errorMessage || `ZeroBounce API error: ${response.status} ${response.statusText}`, + output: { credits: 0 }, + } + } + const credits = Number(data.Credits ?? 0) + return { + success: true, + output: { credits: Number.isNaN(credits) ? 0 : credits }, + } + }, + + outputs: { + credits: { + type: 'number', + description: 'Remaining validation credits (-1 if unavailable)', + }, + }, + } diff --git a/apps/sim/tools/zerobounce/hosting.ts b/apps/sim/tools/zerobounce/hosting.ts new file mode 100644 index 00000000000..807c31fd1a6 --- /dev/null +++ b/apps/sim/tools/zerobounce/hosting.ts @@ -0,0 +1,39 @@ +import type { ToolHostingConfig } from '@/tools/types' + +/** + * Env var prefix for ZeroBounce hosted keys. Provide keys as + * `ZEROBOUNCE_API_KEY_COUNT` plus `ZEROBOUNCE_API_KEY_1..N`. + */ +export const ZEROBOUNCE_API_KEY_PREFIX = 'ZEROBOUNCE_API_KEY' + +/** + * Dollar cost of a single ZeroBounce validation credit. ZeroBounce charges one + * credit per email validated; estimated from the volume credit tiers and + * rounded up for smaller plans — https://www.zerobounce.net/pricing/. + */ +export const ZEROBOUNCE_CREDIT_USD = 0.007 + +/** + * Build a ZeroBounce `hosting` config. `getCredits` returns the number of + * validation credits the call consumed (one per validated email). + */ +export function zerobounceHosting

( + getCredits: (params: P, output: Record) => number +): ToolHostingConfig

{ + return { + envKeyPrefix: ZEROBOUNCE_API_KEY_PREFIX, + apiKeyParam: 'apiKey', + byokProviderId: 'zerobounce', + pricing: { + type: 'custom', + getCost: (params, output) => { + const credits = getCredits(params, output) + return { cost: credits * ZEROBOUNCE_CREDIT_USD, metadata: { credits } } + }, + }, + rateLimit: { + mode: 'per_request', + requestsPerMinute: 60, + }, + } +} diff --git a/apps/sim/tools/zerobounce/index.ts b/apps/sim/tools/zerobounce/index.ts new file mode 100644 index 00000000000..2e4fa4c9d58 --- /dev/null +++ b/apps/sim/tools/zerobounce/index.ts @@ -0,0 +1,7 @@ +export * from './types' + +import { getCreditsTool } from '@/tools/zerobounce/get_credits' +import { verifyEmailTool } from '@/tools/zerobounce/verify_email' + +export const zerobounceVerifyEmailTool = verifyEmailTool +export const zerobounceGetCreditsTool = getCreditsTool diff --git a/apps/sim/tools/zerobounce/types.ts b/apps/sim/tools/zerobounce/types.ts new file mode 100644 index 00000000000..e04dcd59c16 --- /dev/null +++ b/apps/sim/tools/zerobounce/types.ts @@ -0,0 +1,29 @@ +import type { ToolResponse } from '@/tools/types' + +export interface ZeroBounceVerifyEmailParams { + email: string + apiKey: string +} + +export interface ZeroBounceVerifyEmailResponse extends ToolResponse { + output: { + email: string + status: string + deliverable: boolean + subStatus?: string + freeEmail?: boolean + didYouMean?: string + } +} + +export interface ZeroBounceGetCreditsParams { + apiKey: string +} + +export interface ZeroBounceGetCreditsResponse extends ToolResponse { + output: { + credits: number + } +} + +export type ZeroBounceResponse = ZeroBounceVerifyEmailResponse | ZeroBounceGetCreditsResponse diff --git a/apps/sim/tools/zerobounce/verify_email.ts b/apps/sim/tools/zerobounce/verify_email.ts new file mode 100644 index 00000000000..597c5cc9cfe --- /dev/null +++ b/apps/sim/tools/zerobounce/verify_email.ts @@ -0,0 +1,108 @@ +import type { ToolConfig } from '@/tools/types' +import { zerobounceHosting } from '@/tools/zerobounce/hosting' +import type { + ZeroBounceVerifyEmailParams, + ZeroBounceVerifyEmailResponse, +} from '@/tools/zerobounce/types' + +export const verifyEmailTool: ToolConfig< + ZeroBounceVerifyEmailParams, + ZeroBounceVerifyEmailResponse +> = { + id: 'zerobounce_verify_email', + name: 'ZeroBounce Verify Email', + description: 'Validate an email address deliverability in real time. Uses one validation credit.', + version: '1.0.0', + + hosting: zerobounceHosting(() => { + // Each validation consumes one ZeroBounce credit. + return 1 + }), + + params: { + email: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Email address to validate (e.g., john@example.com)', + }, + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ZeroBounce API Key', + }, + }, + + request: { + url: (params) => + `https://api.zerobounce.net/v2/validate?api_key=${encodeURIComponent( + params.apiKey.trim() + )}&email=${encodeURIComponent(params.email.trim())}&ip_address=`, + method: 'GET', + headers: () => ({ Accept: 'application/json' }), + }, + + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + return { + success: false, + error: + (errorData as Record).error || + `ZeroBounce API error: ${response.status} ${response.statusText}`, + output: { email: '', status: '', deliverable: false }, + } + } + const data = await response.json().catch(() => ({})) + if (data.error) { + return { + success: false, + error: String(data.error), + output: { email: '', status: '', deliverable: false }, + } + } + const rawStatus = String(data.status ?? '') + // Normalize ZeroBounce's hyphenated 'catch-all' to the shared 'catch_all' vocabulary. + const status = rawStatus === 'catch-all' ? 'catch_all' : rawStatus + return { + success: true, + output: { + email: data.address ?? '', + status, + deliverable: rawStatus === 'valid', + subStatus: data.sub_status ?? '', + freeEmail: data.free_email ?? false, + didYouMean: data.did_you_mean ?? '', + }, + } + }, + + outputs: { + email: { type: 'string', description: 'The validated email address' }, + status: { + type: 'string', + description: + 'Validation status (valid, invalid, catch_all, unknown, spamtrap, abuse, do_not_mail)', + }, + deliverable: { + type: 'boolean', + description: 'Whether the email is valid and safe to send', + }, + subStatus: { + type: 'string', + description: 'Detailed sub-status from ZeroBounce', + optional: true, + }, + freeEmail: { + type: 'boolean', + description: 'Whether the address is on a free email provider', + optional: true, + }, + didYouMean: { + type: 'string', + description: 'Suggested correction for a likely typo', + optional: true, + }, + }, +}