From d983bb467c31281b7bf8d7b19c3c767f47a07ace Mon Sep 17 00:00:00 2001 From: Abimael Martell Date: Fri, 30 Jan 2026 16:29:27 -0800 Subject: [PATCH 1/4] Add support for local/custom API URLs For custom API URLs (e.g., local development with Docker), the CLI now: - Skips API key requirement when using non-cloud URLs - Allows optional API key input during login - Skips browser auth flow for custom URLs This enables use with local Firecrawl instances that have USE_DB_AUTHENTICATION=false. Co-Authored-By: Claude Opus 4.5 --- src/__tests__/utils/config.test.ts | 45 ++++++++++++++++++++++++++++++ src/commands/login.ts | 10 ++++--- src/utils/auth.ts | 35 ++++++++++++++++++++--- src/utils/config.ts | 16 +++++++++++ 4 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/__tests__/utils/config.test.ts b/src/__tests__/utils/config.test.ts index 0069cbba28..6d2e83d4c2 100644 --- a/src/__tests__/utils/config.test.ts +++ b/src/__tests__/utils/config.test.ts @@ -8,6 +8,8 @@ import { getConfig, resetConfig, updateConfig, + validateConfig, + isCustomApiUrl, } from '../../utils/config'; import { getClient, resetClient } from '../../utils/client'; import * as credentials from '../../utils/credentials'; @@ -231,4 +233,47 @@ describe('Config Fallback Priority', () => { expect(config.apiUrl).toBe('https://url2.com'); // Should be updated }); }); + + describe('isCustomApiUrl', () => { + it('should return false for default cloud API URL', () => { + initializeConfig({ apiUrl: 'https://api.firecrawl.dev' }); + expect(isCustomApiUrl()).toBe(false); + }); + + it('should return true for custom API URLs', () => { + initializeConfig({ apiUrl: 'http://localhost:3002' }); + expect(isCustomApiUrl()).toBe(true); + }); + + it('should return false when no apiUrl is set', () => { + initializeConfig({}); + expect(isCustomApiUrl()).toBe(false); + }); + + it('should accept apiUrl parameter override', () => { + initializeConfig({ apiUrl: 'https://api.firecrawl.dev' }); + expect(isCustomApiUrl('http://localhost:3002')).toBe(true); + }); + }); + + describe('validateConfig with custom API URLs', () => { + it('should not require API key for custom API URLs', () => { + initializeConfig({ apiUrl: 'http://localhost:3002' }); + // Should not throw + expect(() => validateConfig()).not.toThrow(); + }); + + it('should require API key for cloud API URL', () => { + initializeConfig({ apiUrl: 'https://api.firecrawl.dev' }); + expect(() => validateConfig()).toThrow('API key is required'); + }); + + it('should not throw when API key is provided for cloud API', () => { + initializeConfig({ + apiUrl: 'https://api.firecrawl.dev', + apiKey: 'fc-test-key', + }); + expect(() => validateConfig()).not.toThrow(); + }); + }); }); diff --git a/src/commands/login.ts b/src/commands/login.ts index e3e730b068..1a35803baf 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -30,9 +30,10 @@ export async function handleLoginCommand( ): Promise { const apiUrl = options.apiUrl?.replace(/\/$/, '') || DEFAULT_API_URL; const webUrl = options.webUrl?.replace(/\/$/, '') || WEB_URL; + const isCustomUrl = apiUrl !== DEFAULT_API_URL; // If already authenticated, let them know - if (isAuthenticated() && !options.apiKey && !options.method) { + if (isAuthenticated() && !options.apiKey && !options.method && !isCustomUrl) { console.log('You are already logged in.'); console.log(`Credentials stored at: ${getConfigDirectoryPath()}`); console.log('\nTo login with a different account, run:'); @@ -43,7 +44,8 @@ export async function handleLoginCommand( // If API key provided directly, save it if (options.apiKey) { - if (!options.apiKey.startsWith('fc-')) { + // Only validate fc- prefix for cloud API + if (!isCustomUrl && !options.apiKey.startsWith('fc-')) { console.error( 'Error: Invalid API key format. API keys should start with "fc-"' ); @@ -75,11 +77,11 @@ export async function handleLoginCommand( let result: { apiKey: string; apiUrl: string; teamName?: string }; if (options.method === 'manual') { - result = await manualLogin(); + result = await manualLogin(apiUrl); } else if (options.method === 'browser') { result = await browserLogin(webUrl); } else { - result = await interactiveLogin(webUrl); + result = await interactiveLogin(webUrl, apiUrl); } // Save credentials diff --git a/src/utils/auth.ts b/src/utils/auth.ts index e0677cf32d..94f02c37f8 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -494,9 +494,25 @@ async function browserLogin( /** * Perform manual API key login + * For custom API URLs (local development), API key is optional */ -async function manualLogin(): Promise<{ apiKey: string; apiUrl: string }> { +async function manualLogin( + apiUrl: string = DEFAULT_API_URL +): Promise<{ apiKey: string; apiUrl: string }> { + const isCustomUrl = apiUrl !== DEFAULT_API_URL; + console.log(''); + + if (isCustomUrl) { + const apiKey = await promptInput( + 'Enter your API key (press Enter to skip): ' + ); + return { + apiKey: apiKey.trim(), + apiUrl, + }; + } + const apiKey = await promptInput('Enter your Firecrawl API key: '); if (!apiKey || apiKey.trim().length === 0) { @@ -509,7 +525,7 @@ async function manualLogin(): Promise<{ apiKey: string; apiUrl: string }> { return { apiKey: apiKey.trim(), - apiUrl: DEFAULT_API_URL, + apiUrl, }; } @@ -553,8 +569,12 @@ function printBanner(): void { * Interactive login flow - prompts user to choose method */ async function interactiveLogin( - webUrl?: string + webUrl?: string, + apiUrl?: string ): Promise<{ apiKey: string; apiUrl: string; teamName?: string }> { + const effectiveApiUrl = apiUrl || DEFAULT_API_URL; + const isCustomUrl = effectiveApiUrl !== DEFAULT_API_URL; + // First check if env var is set const envResult = envVarLogin(); if (envResult) { @@ -564,6 +584,13 @@ async function interactiveLogin( } printBanner(); + + // For custom URLs (local development), skip browser auth option + if (isCustomUrl) { + console.log(`Configuring CLI for custom API: ${effectiveApiUrl}\n`); + return manualLogin(effectiveApiUrl); + } + console.log( 'Welcome! To get started, authenticate with your Firecrawl account.\n' ); @@ -578,7 +605,7 @@ async function interactiveLogin( const choice = await promptInput('Enter choice [1/2]: '); if (choice === '2' || choice.toLowerCase() === 'manual') { - return manualLogin(); + return manualLogin(effectiveApiUrl); } else { return browserLogin(webUrl); } diff --git a/src/utils/config.ts b/src/utils/config.ts index f75e72d81e..1374a308fc 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -72,10 +72,26 @@ export function getApiKey(providedKey?: string): string | undefined { return storedCredentials?.apiKey; } +const DEFAULT_API_URL = 'https://api.firecrawl.dev'; + +/** + * Check if using a custom (non-cloud) API URL + */ +export function isCustomApiUrl(apiUrl?: string): boolean { + const url = apiUrl || globalConfig.apiUrl; + return !!url && url !== DEFAULT_API_URL; +} + /** * Validate that required configuration is present + * API key is only required for the cloud API, not for local/custom APIs */ export function validateConfig(apiKey?: string): void { + // Skip API key validation for custom API URLs (e.g., local development) + if (isCustomApiUrl()) { + return; + } + const key = getApiKey(apiKey); if (!key) { throw new Error( From 4ed4bde3710ab01ef24e01572ebc82992d0d7150 Mon Sep 17 00:00:00 2001 From: Abimael Martell Date: Fri, 30 Jan 2026 16:43:38 -0800 Subject: [PATCH 2/4] Add --api-url option to commands Add --api-url flag at both global and command levels (scrape, crawl, map, search, credit-usage) similar to --api-key. When a custom API URL is provided, authentication is skipped allowing requests to local or self-hosted Firecrawl instances without an API key. Co-Authored-By: Claude Opus 4.5 --- src/commands/crawl.ts | 4 ++-- src/commands/credit-usage.ts | 11 +++++++---- src/commands/map.ts | 2 +- src/commands/scrape.ts | 4 ++-- src/commands/search.ts | 2 +- src/index.ts | 25 ++++++++++++++++++++++--- src/types/crawl.ts | 2 ++ src/types/map.ts | 2 ++ src/types/scrape.ts | 2 ++ src/types/search.ts | 2 ++ src/utils/options.ts | 1 + 11 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/commands/crawl.ts b/src/commands/crawl.ts index 9d6a5c3705..54daaf621c 100644 --- a/src/commands/crawl.ts +++ b/src/commands/crawl.ts @@ -19,7 +19,7 @@ async function checkCrawlStatus( options: CrawlOptions ): Promise { try { - const app = getClient({ apiKey: options.apiKey }); + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); const status = await app.getCrawlStatus(jobId); return { @@ -48,7 +48,7 @@ export async function executeCrawl( options: CrawlOptions ): Promise { try { - const app = getClient({ apiKey: options.apiKey }); + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); const { urlOrJobId, status, wait, pollInterval, timeout } = options; // If status flag is set or input looks like a job ID, check status diff --git a/src/commands/credit-usage.ts b/src/commands/credit-usage.ts index e4f04de85e..b064f5aefd 100644 --- a/src/commands/credit-usage.ts +++ b/src/commands/credit-usage.ts @@ -21,6 +21,8 @@ export interface CreditUsageResult { export interface CreditUsageOptions { /** API key for Firecrawl */ apiKey?: string; + /** API URL for Firecrawl */ + apiUrl?: string; /** Output file path */ output?: string; /** Output as JSON format */ @@ -36,9 +38,9 @@ export async function executeCreditUsage( options: CreditUsageOptions = {} ): Promise { try { - // Update config if API key provided (via getClient) - if (options.apiKey) { - getClient({ apiKey: options.apiKey }); + // Update config if API key or URL provided (via getClient) + if (options.apiKey || options.apiUrl) { + getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); } // Get config and validate API key @@ -46,7 +48,8 @@ export async function executeCreditUsage( const apiKey = options.apiKey || config.apiKey; validateConfig(apiKey); - const apiUrl = config.apiUrl || 'https://api.firecrawl.dev'; + const apiUrl = + options.apiUrl || config.apiUrl || 'https://api.firecrawl.dev'; // Make the API call to /v2/team/credit-usage const url = `${apiUrl.replace(/\/$/, '')}/v2/team/credit-usage`; diff --git a/src/commands/map.ts b/src/commands/map.ts index 6841dc0343..301e12dca0 100644 --- a/src/commands/map.ts +++ b/src/commands/map.ts @@ -11,7 +11,7 @@ import { writeOutput } from '../utils/output'; */ export async function executeMap(options: MapOptions): Promise { try { - const app = getClient({ apiKey: options.apiKey }); + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); const { urlOrJobId } = options; // Build map options diff --git a/src/commands/scrape.ts b/src/commands/scrape.ts index 66479c19c2..66314cc2aa 100644 --- a/src/commands/scrape.ts +++ b/src/commands/scrape.ts @@ -49,8 +49,8 @@ function outputTiming( export async function executeScrape( options: ScrapeOptions ): Promise { - // Get client instance (updates global config if apiKey provided) - const app = getClient({ apiKey: options.apiKey }); + // Get client instance (updates global config if apiKey/apiUrl provided) + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); // Build scrape options const formats: FormatOption[] = []; diff --git a/src/commands/search.ts b/src/commands/search.ts index 52ece9f153..f2fd5d2a67 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -21,7 +21,7 @@ export async function executeSearch( options: SearchOptions ): Promise { try { - const app = getClient({ apiKey: options.apiKey }); + const app = getClient({ apiKey: options.apiKey, apiUrl: options.apiUrl }); // Build search options for the SDK const searchParams: Record = { diff --git a/src/index.ts b/src/index.ts index f42d28e063..17f40edff6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,20 +48,31 @@ program '-k, --api-key ', 'Firecrawl API key (or set FIRECRAWL_API_KEY env var)' ) + .option('--api-url ', 'API URL (or set FIRECRAWL_API_URL env var)') .option('--status', 'Show version, auth status, concurrency, and credits') .allowUnknownOption() // Allow unknown options when URL is passed directly .hook('preAction', async (thisCommand, actionCommand) => { - // Update global config if API key is provided via global option + // Update global config if API key or URL is provided via global option const globalOptions = thisCommand.opts(); + const commandOptions = actionCommand.opts(); if (globalOptions.apiKey) { updateConfig({ apiKey: globalOptions.apiKey }); } + if (globalOptions.apiUrl) { + updateConfig({ apiUrl: globalOptions.apiUrl }); + } // Check if this command requires authentication const commandName = actionCommand.name(); if (AUTH_REQUIRED_COMMANDS.includes(commandName)) { - // Ensure user is authenticated (prompts for login if needed) - await ensureAuthenticated(); + // Skip auth for custom API URLs (e.g., local development) + // Check both global and command-level options + const { isCustomApiUrl } = await import('./utils/config'); + const effectiveApiUrl = commandOptions.apiUrl || globalOptions.apiUrl; + if (!isCustomApiUrl(effectiveApiUrl)) { + // Ensure user is authenticated (prompts for login if needed) + await ensureAuthenticated(); + } } }); @@ -98,6 +109,7 @@ function createScrapeCommand(): Command { '-k, --api-key ', 'Firecrawl API key (overrides global --api-key)' ) + .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') .option('--json', 'Output as JSON format', false) .option('--pretty', 'Pretty print JSON output', false) @@ -199,6 +211,7 @@ function createCrawlCommand(): Command { '-k, --api-key ', 'Firecrawl API key (overrides global --api-key)' ) + .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') .option('--pretty', 'Pretty print JSON output', false) .action(async (positionalUrlOrJobId, options) => { @@ -224,6 +237,7 @@ function createCrawlCommand(): Command { output: options.output, pretty: options.pretty, apiKey: options.apiKey, + apiUrl: options.apiUrl, limit: options.limit, maxDepth: options.maxDepth, excludePaths: options.excludePaths @@ -273,6 +287,7 @@ function createMapCommand(): Command { '-k, --api-key ', 'Firecrawl API key (overrides global --api-key)' ) + .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') .option('--json', 'Output as JSON format', false) .option('--pretty', 'Pretty print JSON output', false) @@ -293,6 +308,7 @@ function createMapCommand(): Command { json: options.json, pretty: options.pretty, apiKey: options.apiKey, + apiUrl: options.apiUrl, limit: options.limit, search: options.search, sitemap: options.sitemap, @@ -363,6 +379,7 @@ function createSearchCommand(): Command { '-k, --api-key ', 'Firecrawl API key (overrides global --api-key)' ) + .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') // .option( // '-p, --pretty', @@ -431,6 +448,7 @@ function createSearchCommand(): Command { scrapeFormats, onlyMainContent: options.onlyMainContent, apiKey: options.apiKey, + apiUrl: options.apiUrl, output: options.output, json: options.json, pretty: options.pretty, @@ -520,6 +538,7 @@ program '-k, --api-key ', 'Firecrawl API key (overrides global --api-key)' ) + .option('--api-url ', 'API URL (overrides global --api-url)') .option('-o, --output ', 'Output file path (default: stdout)') .option('--json', 'Output as JSON format', false) .option( diff --git a/src/types/crawl.ts b/src/types/crawl.ts index 4efca802d3..5fc15e8fb6 100644 --- a/src/types/crawl.ts +++ b/src/types/crawl.ts @@ -5,6 +5,8 @@ export interface CrawlOptions { /** API key for Firecrawl */ apiKey?: string; + /** API URL for Firecrawl */ + apiUrl?: string; /** URL to crawl or job ID to check status */ urlOrJobId: string; /** Check status of existing crawl job */ diff --git a/src/types/map.ts b/src/types/map.ts index 843a30550e..07bf3e8fc6 100644 --- a/src/types/map.ts +++ b/src/types/map.ts @@ -5,6 +5,8 @@ export interface MapOptions { /** API key for Firecrawl */ apiKey?: string; + /** API URL for Firecrawl */ + apiUrl?: string; /** URL to map or job ID to check status */ urlOrJobId: string; /** Check status of existing map job */ diff --git a/src/types/scrape.ts b/src/types/scrape.ts index cee2b981eb..aaff2e1f9d 100644 --- a/src/types/scrape.ts +++ b/src/types/scrape.ts @@ -32,6 +32,8 @@ export interface ScrapeOptions { excludeTags?: string[]; /** API key for Firecrawl */ apiKey?: string; + /** API URL for Firecrawl */ + apiUrl?: string; /** Output file path */ output?: string; /** Pretty print JSON output */ diff --git a/src/types/search.ts b/src/types/search.ts index 57d0f359d2..e469b2b8cc 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -12,6 +12,8 @@ export interface SearchOptions { query: string; /** API key for Firecrawl */ apiKey?: string; + /** API URL for Firecrawl */ + apiUrl?: string; /** Maximum number of results (default: 5, max: 100) */ limit?: number; /** Sources to search: web, images, news (default: web) */ diff --git a/src/utils/options.ts b/src/utils/options.ts index 626090d2f4..e4bc869679 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -85,6 +85,7 @@ export function parseScrapeOptions(options: any): ScrapeOptions { ? options.excludeTags.split(',').map((t: string) => t.trim()) : undefined, apiKey: options.apiKey, + apiUrl: options.apiUrl, output: options.output, pretty: options.pretty, json: options.json, From 115511abbe8393c0ba6e8ca18b67413c83091623 Mon Sep 17 00:00:00 2001 From: Abimael Martell Date: Fri, 30 Jan 2026 16:44:55 -0800 Subject: [PATCH 3/4] docs: Add --api-url option documentation Document the --api-url option for self-hosted and local development use cases in the README, including examples for environment variables and CI/CD usage. Co-Authored-By: Claude Opus 4.5 --- README.md | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dd6ac364fa..51597f6d9b 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,24 @@ export FIRECRAWL_API_KEY=fc-your-api-key firecrawl scrape https://example.com --api-key fc-your-api-key ``` +### Self-hosted / Local Development + +For self-hosted Firecrawl instances or local development, use the `--api-url` option: + +```bash +# Use a local Firecrawl instance (no API key required) +firecrawl --api-url http://localhost:3002 scrape https://example.com + +# Or set via environment variable +export FIRECRAWL_API_URL=http://localhost:3002 +firecrawl scrape https://example.com + +# Self-hosted with API key +firecrawl --api-url https://firecrawl.mycompany.com --api-key fc-xxx scrape https://example.com +``` + +When using a custom API URL (anything other than `https://api.firecrawl.dev`), authentication is automatically skipped, allowing you to use local instances without an API key. + --- ## Commands @@ -369,12 +387,13 @@ firecrawl logout These options work with any command: -| Option | Description | -| --------------------- | -------------------------------------------- | -| `--status` | Show version, auth, concurrency, and credits | -| `-k, --api-key ` | Use specific API key | -| `-V, --version` | Show version | -| `-h, --help` | Show help | +| Option | Description | +| --------------------- | ------------------------------------------------------ | +| `--status` | Show version, auth, concurrency, and credits | +| `-k, --api-key ` | Use specific API key | +| `--api-url ` | Use custom API URL (for self-hosted/local development) | +| `-V, --version` | Show version | +| `-h, --help` | Show help | ### Check Status @@ -458,6 +477,10 @@ firecrawl https://example.com | grep -i "keyword" # Set API key via environment export FIRECRAWL_API_KEY=${{ secrets.FIRECRAWL_API_KEY }} firecrawl crawl https://docs.example.com --wait -o docs.json + +# Use self-hosted instance +export FIRECRAWL_API_URL=${{ secrets.FIRECRAWL_API_URL }} +firecrawl scrape https://example.com -o output.md ``` --- From 018000526f180506b54777ce866abd9584d0990b Mon Sep 17 00:00:00 2001 From: Abimael Martell Date: Fri, 30 Jan 2026 16:46:34 -0800 Subject: [PATCH 4/4] docs: Add --api-url to login and config examples Co-Authored-By: Claude Opus 4.5 --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 51597f6d9b..68865660a3 100644 --- a/README.md +++ b/README.md @@ -358,10 +358,15 @@ firecrawl credit-usage --json --pretty --- -### `config` - View configuration +### `config` - Configure and view settings ```bash +# View current configuration firecrawl config + +# Configure with custom API URL +firecrawl config --api-url https://firecrawl.mycompany.com +firecrawl config --api-url http://localhost:3002 --api-key fc-xxx ``` Shows authentication status and stored credentials location. @@ -377,6 +382,10 @@ firecrawl login --method browser firecrawl login --method manual firecrawl login --api-key fc-xxx +# Login to self-hosted instance +firecrawl login --api-url https://firecrawl.mycompany.com +firecrawl login --api-url http://localhost:3002 --api-key fc-xxx + # Logout firecrawl logout ```