diff --git a/plugins/hunter/credentials.ts b/plugins/hunter/credentials.ts
new file mode 100644
index 00000000..842616b6
--- /dev/null
+++ b/plugins/hunter/credentials.ts
@@ -0,0 +1,3 @@
+export type HunterCredentials = {
+ HUNTER_API_KEY?: string;
+};
diff --git a/plugins/hunter/icon.tsx b/plugins/hunter/icon.tsx
new file mode 100644
index 00000000..57665d58
--- /dev/null
+++ b/plugins/hunter/icon.tsx
@@ -0,0 +1,14 @@
+export function HunterIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/plugins/hunter/index.ts b/plugins/hunter/index.ts
new file mode 100644
index 00000000..1be00c8f
--- /dev/null
+++ b/plugins/hunter/index.ts
@@ -0,0 +1,69 @@
+import type { IntegrationPlugin } from "../registry";
+import { registerIntegration } from "../registry";
+import { HunterIcon } from "./icon";
+
+const hunterPlugin: IntegrationPlugin = {
+ type: "hunter",
+ label: "Hunter",
+ description: "All-in-one email outreach platform",
+ icon: HunterIcon,
+
+ formFields: [
+ {
+ id: "apiKey",
+ label: "API Key",
+ type: "password",
+ placeholder: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ configKey: "apiKey",
+ envVar: "HUNTER_API_KEY",
+ helpText: "Get your API key from ",
+ helpLink: {
+ text: "hunter.io/api-keys",
+ url: "https://hunter.io/api-keys",
+ },
+ },
+ ],
+
+ testConfig: {
+ getTestFunction: async () => {
+ const { testHunter } = await import("./test");
+ return testHunter;
+ },
+ },
+
+ actions: [
+ {
+ slug: "enrich-lead",
+ label: "Enrich Lead",
+ description: "Get data on a lead and their company",
+ category: "Hunter",
+ stepFunction: "enrichLeadStep",
+ stepImportPath: "enrich-lead",
+ configFields: [
+ {
+ key: "email",
+ label: "Lead Email",
+ type: "template-input",
+ placeholder: "{{Trigger.email}} or john@example.com",
+ example: "john@example.com",
+ required: true,
+ },
+ {
+ key: "enrichmentType",
+ label: "Enrichment Type",
+ type: "select",
+ options: [
+ { value: "individual", label: "Individual" },
+ { value: "company", label: "Company" },
+ { value: "combined", label: "Combined" },
+ ],
+ defaultValue: "combined",
+ },
+ ],
+ },
+ ],
+};
+
+registerIntegration(hunterPlugin);
+
+export default hunterPlugin;
diff --git a/plugins/hunter/steps/enrich-lead.ts b/plugins/hunter/steps/enrich-lead.ts
new file mode 100644
index 00000000..6a607ae0
--- /dev/null
+++ b/plugins/hunter/steps/enrich-lead.ts
@@ -0,0 +1,307 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import { getErrorMessage } from "@/lib/utils";
+import type { HunterCredentials } from "../credentials";
+
+type PersonData = {
+ fullName?: string;
+ givenName?: string;
+ familyName?: string;
+ email?: string;
+ location?: string;
+ timezone?: string;
+ title?: string;
+ role?: string;
+ seniority?: string;
+ domain?: string;
+ company?: string;
+ twitter?: string;
+ linkedin?: string;
+ phone?: string;
+};
+
+type CompanyData = {
+ domain?: string;
+ name?: string;
+ headcount?: string;
+ industry?: string;
+ country?: string;
+ state?: string;
+ city?: string;
+ twitter?: string;
+ linkedin?: string;
+ facebook?: string;
+};
+
+type EnrichLeadResult =
+ | {
+ success: true;
+ enrichmentType: string;
+ person?: PersonData;
+ company?: CompanyData;
+ }
+ | { success: false; error: string };
+
+export type EnrichLeadCoreInput = {
+ email: string;
+ enrichmentType?: "individual" | "company" | "combined";
+};
+
+function isValidEmail(email: string): boolean {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return emailRegex.test(email);
+}
+
+export type EnrichLeadInput = StepInput &
+ EnrichLeadCoreInput & {
+ integrationId?: string;
+ };
+
+async function fetchPersonData(
+ email: string,
+ apiKey: string
+): Promise {
+ const url = new URL("https://api.hunter.io/v2/people/find");
+ url.searchParams.set("email", email);
+ url.searchParams.set("api_key", apiKey);
+
+ const response = await fetch(url.toString(), {
+ method: "GET",
+ headers: { "Content-Type": "application/json" },
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return null;
+ }
+ const error = await response.json().catch(() => ({}));
+ throw new Error(
+ error.errors?.[0]?.details || `HTTP ${response.status}: ${response.statusText}`
+ );
+ }
+
+ const result = await response.json();
+ const data = result.data;
+
+ if (!data) {
+ return null;
+ }
+
+ return {
+ fullName: data.name?.fullName,
+ givenName: data.name?.givenName,
+ familyName: data.name?.familyName,
+ email: data.email,
+ location: data.location,
+ timezone: data.timeZone,
+ title: data.employment?.title,
+ role: data.employment?.role,
+ seniority: data.employment?.seniority,
+ domain: data.employment?.domain,
+ company: data.employment?.name,
+ twitter: data.twitter?.handle,
+ linkedin: data.linkedin?.handle,
+ phone: data.phone,
+ };
+}
+
+async function fetchCompanyData(
+ domain: string,
+ apiKey: string
+): Promise {
+ const url = new URL("https://api.hunter.io/v2/companies/find");
+ url.searchParams.set("domain", domain);
+ url.searchParams.set("api_key", apiKey);
+
+ const response = await fetch(url.toString(), {
+ method: "GET",
+ headers: { "Content-Type": "application/json" },
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return null;
+ }
+ const error = await response.json().catch(() => ({}));
+ throw new Error(
+ error.errors?.[0]?.details || `HTTP ${response.status}: ${response.statusText}`
+ );
+ }
+
+ const result = await response.json();
+ const data = result.data;
+
+ if (!data) {
+ return null;
+ }
+
+ return {
+ domain: data.domain,
+ name: data.name,
+ headcount: data.headcount,
+ industry: data.industry,
+ country: data.country,
+ state: data.state,
+ city: data.city,
+ twitter: data.twitter,
+ linkedin: data.linkedin,
+ facebook: data.facebook,
+ };
+}
+
+async function fetchCombinedData(
+ email: string,
+ apiKey: string
+): Promise<{ person: PersonData | null; company: CompanyData | null }> {
+ const url = new URL("https://api.hunter.io/v2/combined/find");
+ url.searchParams.set("email", email);
+ url.searchParams.set("api_key", apiKey);
+
+ const response = await fetch(url.toString(), {
+ method: "GET",
+ headers: { "Content-Type": "application/json" },
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { person: null, company: null };
+ }
+ const error = await response.json().catch(() => ({}));
+ throw new Error(
+ error.errors?.[0]?.details || `HTTP ${response.status}: ${response.statusText}`
+ );
+ }
+
+ const result = await response.json();
+ const data = result.data;
+
+ if (!data) {
+ return { person: null, company: null };
+ }
+
+ const person: PersonData | null = data.person
+ ? {
+ fullName: data.person.name?.fullName,
+ givenName: data.person.name?.givenName,
+ familyName: data.person.name?.familyName,
+ email: data.person.email,
+ location: data.person.location,
+ timezone: data.person.timeZone,
+ title: data.person.employment?.title,
+ role: data.person.employment?.role,
+ seniority: data.person.employment?.seniority,
+ domain: data.person.employment?.domain,
+ company: data.person.employment?.name,
+ twitter: data.person.twitter?.handle,
+ linkedin: data.person.linkedin?.handle,
+ phone: data.person.phone,
+ }
+ : null;
+
+ const company: CompanyData | null = data.company
+ ? {
+ domain: data.company.domain,
+ name: data.company.name,
+ headcount: data.company.headcount,
+ industry: data.company.industry,
+ country: data.company.country,
+ state: data.company.state,
+ city: data.company.city,
+ twitter: data.company.twitter,
+ linkedin: data.company.linkedin,
+ facebook: data.company.facebook,
+ }
+ : null;
+
+ return { person, company };
+}
+
+function extractDomain(email: string): string | null {
+ const parts = email.split("@");
+ if (parts.length !== 2) {
+ return null;
+ }
+ return parts[1];
+}
+
+async function stepHandler(
+ input: EnrichLeadCoreInput,
+ credentials: HunterCredentials
+): Promise {
+ const apiKey = credentials.HUNTER_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "HUNTER_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ const enrichmentType = input.enrichmentType || "combined";
+ const email = input.email.trim();
+
+ if (!isValidEmail(email)) {
+ return {
+ success: false,
+ error: "Invalid email address format",
+ };
+ }
+
+ try {
+ let person: PersonData | null = null;
+ let company: CompanyData | null = null;
+
+ if (enrichmentType === "individual") {
+ person = await fetchPersonData(email, apiKey);
+ } else if (enrichmentType === "company") {
+ const domain = extractDomain(email);
+ if (!domain) {
+ return {
+ success: false,
+ error: "Invalid email format - could not extract domain",
+ };
+ }
+ company = await fetchCompanyData(domain, apiKey);
+ } else {
+ const result = await fetchCombinedData(email, apiKey);
+ person = result.person;
+ company = result.company;
+ }
+
+ if (!person && !company) {
+ return {
+ success: false,
+ error: "No enrichment data found for the provided identifier",
+ };
+ }
+
+ return {
+ success: true,
+ enrichmentType,
+ ...(person && { person }),
+ ...(company && { company }),
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to enrich lead: ${getErrorMessage(error)}`,
+ };
+ }
+}
+
+export async function enrichLeadStep(
+ input: EnrichLeadInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+
+export const _integrationType = "hunter";
diff --git a/plugins/hunter/test.ts b/plugins/hunter/test.ts
new file mode 100644
index 00000000..85049d91
--- /dev/null
+++ b/plugins/hunter/test.ts
@@ -0,0 +1,39 @@
+export async function testHunter(credentials: Record) {
+ try {
+ const apiKey = credentials.HUNTER_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error: "HUNTER_API_KEY is required",
+ };
+ }
+
+ const url = new URL("https://api.hunter.io/v2/account");
+ url.searchParams.set("api_key", apiKey);
+
+ const response = await fetch(url.toString(), {
+ method: "GET",
+ headers: { "Content-Type": "application/json" },
+ });
+
+ if (response.ok) {
+ return { success: true };
+ }
+
+ if (response.status === 401) {
+ return { success: false, error: "Invalid API key" };
+ }
+
+ const error = await response.json().catch(() => ({}));
+ return {
+ success: false,
+ error: error.errors?.[0]?.details || `API error: HTTP ${response.status}`,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
diff --git a/plugins/index.ts b/plugins/index.ts
index c2b41249..6001708d 100644
--- a/plugins/index.ts
+++ b/plugins/index.ts
@@ -19,6 +19,7 @@ import "./blob";
import "./fal";
import "./firecrawl";
import "./github";
+import "./hunter";
import "./linear";
import "./perplexity";
import "./resend";