diff --git a/plugins/brandfetch/credentials.ts b/plugins/brandfetch/credentials.ts
new file mode 100644
index 00000000..8c0082fb
--- /dev/null
+++ b/plugins/brandfetch/credentials.ts
@@ -0,0 +1,3 @@
+export type BrandfetchCredentials = {
+ BRANDFETCH_API_KEY?: string;
+};
diff --git a/plugins/brandfetch/icon.tsx b/plugins/brandfetch/icon.tsx
new file mode 100644
index 00000000..68455560
--- /dev/null
+++ b/plugins/brandfetch/icon.tsx
@@ -0,0 +1,14 @@
+export function BrandfetchIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/plugins/brandfetch/index.ts b/plugins/brandfetch/index.ts
new file mode 100644
index 00000000..d95a2f67
--- /dev/null
+++ b/plugins/brandfetch/index.ts
@@ -0,0 +1,81 @@
+import type { IntegrationPlugin } from "../registry";
+import { registerIntegration } from "../registry";
+import { BrandfetchIcon } from "./icon";
+
+const brandfetchPlugin: IntegrationPlugin = {
+ type: "brandfetch",
+ label: "Brandfetch",
+ description: "Get brand assets and company data",
+
+ icon: BrandfetchIcon,
+
+ formFields: [
+ {
+ id: "apiKey",
+ label: "API Key",
+ type: "password",
+ placeholder: "your-api-key",
+ configKey: "apiKey",
+ envVar: "BRANDFETCH_API_KEY",
+ helpText: "Get your API key from ",
+ helpLink: {
+ text: "Brandfetch Dashboard",
+ url: "https://developers.brandfetch.com/dashboard/keys",
+ },
+ },
+ ],
+
+ testConfig: {
+ getTestFunction: async () => {
+ const { testBrandfetch } = await import("./test");
+ return testBrandfetch;
+ },
+ },
+
+ actions: [
+ {
+ slug: "get-brand",
+ label: "Get Brand",
+ description: "Get a company's brand assets by domain, ticker, or ISIN",
+ category: "Brandfetch",
+ stepFunction: "getBrandStep",
+ stepImportPath: "get-brand",
+ outputFields: [
+ { field: "name", description: "Brand name" },
+ { field: "domain", description: "Brand domain" },
+ { field: "description", description: "Brand description" },
+ { field: "logoUrl", description: "Primary logo URL" },
+ { field: "iconUrl", description: "Icon/symbol URL" },
+ { field: "colors", description: "Array of brand colors (hex)" },
+ { field: "links", description: "Social media and website links" },
+ { field: "industry", description: "Primary industry" },
+ ],
+ configFields: [
+ {
+ key: "identifierType",
+ label: "Identifier Type",
+ type: "select",
+ options: [
+ { value: "domain", label: "Domain" },
+ { value: "ticker", label: "Stock Ticker" },
+ { value: "isin", label: "ISIN" },
+ ],
+ defaultValue: "domain",
+ required: true,
+ },
+ {
+ key: "identifier",
+ label: "Identifier",
+ type: "template-input",
+ placeholder: "nike.com / NKE / US6541061031",
+ example: "nike.com",
+ required: true,
+ },
+ ],
+ },
+ ],
+};
+
+registerIntegration(brandfetchPlugin);
+
+export default brandfetchPlugin;
diff --git a/plugins/brandfetch/steps/get-brand.ts b/plugins/brandfetch/steps/get-brand.ts
new file mode 100644
index 00000000..3203d739
--- /dev/null
+++ b/plugins/brandfetch/steps/get-brand.ts
@@ -0,0 +1,217 @@
+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 { BrandfetchCredentials } from "../credentials";
+
+const BRANDFETCH_API_URL = "https://api.brandfetch.io/v2";
+
+type BrandfetchLogo = {
+ type: string;
+ theme: string;
+ formats: Array<{
+ src: string;
+ format: string;
+ }>;
+};
+
+type BrandfetchColor = {
+ hex: string;
+ type: string;
+ brightness: number;
+};
+
+type BrandfetchLink = {
+ name: string;
+ url: string;
+};
+
+type BrandfetchIndustry = {
+ name: string;
+ slug: string;
+ score: number;
+};
+
+type BrandfetchResponse = {
+ id: string;
+ name: string;
+ domain: string;
+ description?: string;
+ longDescription?: string;
+ logos?: BrandfetchLogo[];
+ colors?: BrandfetchColor[];
+ links?: BrandfetchLink[];
+ company?: {
+ industries?: BrandfetchIndustry[];
+ };
+};
+
+type GetBrandResult =
+ | {
+ success: true;
+ name: string;
+ domain: string;
+ description: string;
+ logoUrl: string;
+ iconUrl: string;
+ colors: string[];
+ links: Array<{ name: string; url: string }>;
+ industry: string;
+ }
+ | { success: false; error: string };
+
+export type GetBrandCoreInput = {
+ identifierType: "domain" | "ticker" | "isin";
+ identifier: string;
+};
+
+export type GetBrandInput = StepInput &
+ GetBrandCoreInput & {
+ integrationId?: string;
+ };
+
+function findLogoUrl(logos: BrandfetchLogo[] | undefined, type: string): string {
+ if (!logos) return "";
+
+ const logo = logos.find((l) => l.type === type);
+ if (!logo || !logo.formats || logo.formats.length === 0) return "";
+
+ // Prefer PNG or SVG
+ const pngFormat = logo.formats.find((f) => f.format === "png");
+ if (pngFormat) return pngFormat.src;
+
+ const svgFormat = logo.formats.find((f) => f.format === "svg");
+ if (svgFormat) return svgFormat.src;
+
+ return logo.formats[0].src;
+}
+
+async function stepHandler(
+ input: GetBrandCoreInput,
+ credentials: BrandfetchCredentials
+): Promise {
+ const apiKey = credentials.BRANDFETCH_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "BRANDFETCH_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!input.identifier) {
+ return {
+ success: false,
+ error: "Identifier is required",
+ };
+ }
+
+ // Validate identifier format based on type
+ let identifier = input.identifier.trim();
+ const identifierType = input.identifierType || "domain";
+
+ if (identifierType === "domain") {
+ // Validate domain format: must have at least one character before and after the dot, no spaces, no leading/trailing dot
+ if (
+ !/^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?(\.[a-zA-Z]{2,})+$/.test(identifier)
+ ) {
+ return {
+ success: false,
+ error: "Invalid domain format. Expected format: example.com",
+ };
+ }
+ } else if (identifierType === "ticker") {
+ identifier = identifier.toUpperCase();
+ if (!/^[A-Z]{1,5}$/.test(identifier)) {
+ return {
+ success: false,
+ error: "Invalid ticker format. Expected 1-5 uppercase letters (e.g., NKE)",
+ };
+ }
+ } else if (identifierType === "isin") {
+ identifier = identifier.toUpperCase();
+ if (!/^[A-Z]{2}[A-Z0-9]{10}$/.test(identifier)) {
+ return {
+ success: false,
+ error: "Invalid ISIN format. Expected 12 characters starting with country code (e.g., US6541061031)",
+ };
+ }
+ }
+
+ try {
+ const response = await fetch(
+ `${BRANDFETCH_API_URL}/brands/${encodeURIComponent(identifier)}`,
+ {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return {
+ success: false,
+ error: `Brand not found for: ${identifier}`,
+ };
+ }
+ const errorData = (await response.json().catch(() => ({}))) as {
+ message?: string;
+ };
+ return {
+ success: false,
+ error: errorData.message || `HTTP ${response.status}`,
+ };
+ }
+
+ const brand = (await response.json()) as BrandfetchResponse;
+
+ // Extract primary logo and icon URLs
+ const logoUrl = findLogoUrl(brand.logos, "logo");
+ const iconUrl = findLogoUrl(brand.logos, "icon") || findLogoUrl(brand.logos, "symbol");
+
+ // Extract colors (just hex values)
+ const colors = brand.colors?.map((c) => c.hex) || [];
+
+ // Extract links
+ const links = brand.links?.map((l) => ({ name: l.name, url: l.url })) || [];
+
+ // Get primary industry
+ const industry = brand.company?.industries?.[0]?.name || "";
+
+ return {
+ success: true,
+ name: brand.name || "",
+ domain: brand.domain || "",
+ description: brand.description || brand.longDescription || "",
+ logoUrl,
+ iconUrl,
+ colors,
+ links,
+ industry,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to get brand: ${getErrorMessage(error)}`,
+ };
+ }
+}
+
+export async function getBrandStep(
+ input: GetBrandInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+getBrandStep.maxRetries = 0;
+
+export const _integrationType = "brandfetch";
diff --git a/plugins/brandfetch/test.ts b/plugins/brandfetch/test.ts
new file mode 100644
index 00000000..78e75e49
--- /dev/null
+++ b/plugins/brandfetch/test.ts
@@ -0,0 +1,42 @@
+const BRANDFETCH_API_URL = "https://api.brandfetch.io/v2";
+
+export async function testBrandfetch(credentials: Record) {
+ try {
+ const apiKey = credentials.BRANDFETCH_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error: "BRANDFETCH_API_KEY is required",
+ };
+ }
+
+ // Use brandfetch.com domain for testing (free and doesn't count against quota)
+ const response = await fetch(`${BRANDFETCH_API_URL}/brands/brandfetch.com`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ return {
+ success: false,
+ error: "Invalid API key. Please check your Brandfetch API key.",
+ };
+ }
+ return {
+ success: false,
+ error: `API validation failed: HTTP ${response.status}`,
+ };
+ }
+
+ return { success: true };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
diff --git a/plugins/index.ts b/plugins/index.ts
index 495c6e33..ceea3782 100644
--- a/plugins/index.ts
+++ b/plugins/index.ts
@@ -16,6 +16,7 @@
import "./ai-gateway";
import "./blob";
+import "./brandfetch";
import "./clerk";
import "./fal";
import "./firecrawl";