Migrate from Firecrawl in One Afternoon
Goal: Move a working Firecrawl integration to fastCRW in a single afternoon. This guide covers the four mechanical changes (base URL, SDK package, env var, extraction shape) plus the behavioral differences you must validate before going to production.
Time estimate: 15 minutes of code changes, 30–60 minutes of validation.
What transfers without changes
The core request shape is intentionally compatible. These work against fastCRW with no modification when you point the client at the right base URL:
/v1/scrape— same body fields (url,formats,onlyMainContent,waitFor,maxAge, etc.)/v1/crawl+/v1/crawl/{id}polling/v1/map/v1/searchAuthorization: Bearer <key>header
Step 1 — Swap the base URL
Every engine call goes to https://api.fastcrw.com instead of https://api.firecrawl.dev.
curl -X POST https://api.firecrawl.dev/v1/scrape \
-H "Authorization: Bearer $FIRECRAWL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://news.ycombinator.com",
"formats": ["markdown"]
}'curl -X POST https://api.fastcrw.com/v1/scrape \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://news.ycombinator.com",
"formats": ["markdown"]
}'Self-hosted: if you run your own crw-server, use your server's address (e.g. http://crw:3000) wherever this guide says https://api.fastcrw.com. Authentication is optional on self-hosted; disable it with [server] auth_required = false in your config.toml.
:::note{title="Keeping the Firecrawl SDK? Change one line"}
You don't have to swap SDKs. Point your existing Firecrawl client at
https://compat.fastcrw.com and it works. Same string in every language, no
path suffix, nothing else to change:
const app = new FirecrawlApp({
apiKey: process.env.CRW_API_KEY,
apiUrl: "https://compat.fastcrw.com",
});
app = FirecrawlApp(
api_key=os.environ["CRW_API_KEY"],
api_url="https://compat.fastcrw.com",
)
That host exists because the SDKs disagree about base paths. firecrawl-py's v2
client resolves each endpoint against the base origin and drops any path you
put there, while the JS client and firecrawl-py's v1 client keep it. A hostname
is the one form all of them honour.
The v2 SDK is the supported drop-in; the legacy v1 SDK is compatible only for
scrape.
Self-hosting? Ignore all of this and use your own address
(http://crw:3000). Your engine serves every surface at the root already.
:::
:::warning{title="Scheduled change: the root /v2 alias"}
https://api.fastcrw.com/v2/* still answers and is what earlier versions of this
guide told Python users to use. It is scheduled to close in October 2026, so
that api.fastcrw.com serves only our native /v1 API.
If you are on it, move your base URL to https://compat.fastcrw.com. Nothing else
changes: same shapes, same keys, same credits. Self-hosted deployments are not
affected.
:::
Step 2 — Swap the SDK package
Python
pip install firecrawl-pyfrom firecrawl import FirecrawlApp
app = FirecrawlApp(api_key="fc-...")
result = app.scrape_url(
"https://news.ycombinator.com",
params={"formats": ["markdown"]},
)
print(result["markdown"][:500])pip install crw
from crw import CrwClient
client = CrwClient() # reads CRW_API_KEY from env
result = client.scrape(
"https://news.ycombinator.com",
formats=["markdown"],
)
print(result["markdown"][:500])TypeScript / JavaScript
npm install @mendable/firecrawl-jsimport FirecrawlApp from "@mendable/firecrawl-js";
const app = new FirecrawlApp({ apiKey: "fc-..." });
const result = await app.scrapeUrl("https://news.ycombinator.com", {
formats: ["markdown"],
});
console.log(result.markdown?.slice(0, 500));npm install crw-sdk
import { CrwClient } from "crw-sdk";
const client = new CrwClient({ apiKey: process.env.CRW_API_KEY });
const result = await client.scrape("https://news.ycombinator.com", {
formats: ["markdown"],
});
console.log(result.markdown?.slice(0, 500));Step 3 — Rename the environment variable
| Old var | New var | Purpose |
|---|---|---|
FIRECRAWL_API_KEY |
CRW_API_KEY |
Engine API key |
FIRECRAWL_API_URL |
CRW_API_URL |
Custom engine URL (self-hosted) |
Shell (.env or export):
# remove
# export FIRECRAWL_API_KEY=fc-...
# add
export CRW_API_KEY=crw-...
Both SDKs read the variable automatically. No code change is needed once the env var is in place.
Step 4 — Port LLM extraction
Firecrawl exposes a standalone /v1/extract route. fastCRW supports both styles: single-URL extraction runs through /v1/scrape with formats: ["json"] and a jsonSchema (response field data.json), and multi-URL extraction has a native async POST /v1/extract (URLs + prompt/schema) that returns a per-URL results array via GET /v1/extract/{id}.
Multi-URL extraction: send { "urls": [...], "prompt"/"schema": ... } to /v1/extract, then poll /v1/extract/{id} for results (one { url, status, data, error } per URL).
from firecrawl import FirecrawlApp
app = FirecrawlApp(api_key="fc-...")
schema = {
"type": "object",
"properties": {
"title": {"type": "string"},
"price": {"type": "string"},
},
"required": ["title", "price"],
}
result = app.extract(
["https://example.com/product"],
{"schema": schema},
)
print(result)from crw import CrwClient
client = CrwClient() # reads CRW_API_KEY from env
schema = {
"type": "object",
"properties": {
"title": {"type": "string"},
"price": {"type": "string"},
},
"required": ["title", "price"],
}
# Pass json_schema — the SDK adds "json" to formats automatically
result = client.scrape(
"https://example.com/product",
json_schema=schema,
)
print(result["json"])import FirecrawlApp from "@mendable/firecrawl-js";
const app = new FirecrawlApp({ apiKey: "fc-..." });
const schema = {
type: "object",
properties: {
title: { type: "string" },
price: { type: "string" },
},
required: ["title", "price"],
};
const result = await app.extract(["https://example.com/product"], {
schema,
});
console.log(result.data);import { CrwClient } from "crw-sdk";
const client = new CrwClient({ apiKey: process.env.CRW_API_KEY });
const schema = {
type: "object",
properties: {
title: { type: "string" },
price: { type: "string" },
},
required: ["title", "price"],
};
// Pass jsonSchema — the SDK adds "json" to formats automatically
const result = await client.scrape("https://example.com/product", {
jsonSchema: schema,
});
console.log(result.json);Behavioral differences to validate
The following table lists the gaps documented in the capability matrix. Validate each that applies to your workload before switching production traffic.
| Feature | Firecrawl | fastCRW | Action required |
|---|---|---|---|
/v1/extract route |
Standalone async route | Supported — POST /v1/extract returns a job id you poll on GET /v1/extract/{id} |
None. /v1/scrape + jsonSchema also works for a single URL. |
Multi-URL /extract |
One call → N URLs | Supported — pass urls: [...] (capped by limits.maxExtractUrls, default 50) |
None. Check the cap on GET /v1/capabilities. |
/v1/deep-research |
Cloud-only Firecrawl feature | Not implemented | No equivalent path — remove or redesign. |
/v1/agent (Spark models) |
Cloud-only Firecrawl feature | Not implemented | No equivalent path. |
/firecrawl/v2/parse file types |
PDF, DOCX, XLSX, ODT, RTF | PDF only (pure-Rust pdf-inspector, no OCR) |
If you upload non-PDF files or rely on OCR, keep Firecrawl for those calls. |
| OCR mode on PDF | mode: "ocr" supported |
Accepted for wire-compat; falls back to text-layer extraction with pdf_scanned warning |
Scanned-only PDFs won't extract text. |
| Fire-engine anti-bot | Firecrawl Cloud only | Not available (same as Firecrawl self-host) | For heavy bot-protected pages, compare output quality on real targets. |
| Screenshot format | Supported | Supported on an instance with a capture-capable browser tier (Chrome or Playwright); LightPanda and Camoufox cannot capture | Check screenshot.supported on GET /v1/capabilities before relying on it. |
data.metadata field names |
Some keys differ | Minor divergence on a few keys | Inspect metadata on a real response; don't assume key names are identical. |
| MCP tool names | firecrawl_scrape, firecrawl_crawl, … |
crw_scrape, crw_crawl, crw_check_crawl_status, crw_map, crw_extract, crw_check_extract_status, crw_cancel_extract, crw_search, crw_parse_file |
Update any MCP client tool-name references. |
5-minute verification checklist
Run these five commands against a real URL from your workload after making the changes above. Use a URL you can manually inspect in your browser.
1. Smoke test — basic scrape
curl -s -X POST https://api.fastcrw.com/v1/scrape \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://news.ycombinator.com", "formats": ["markdown"]}' \
| python3 -m json.tool | head -30
Expected: "success": true, "markdown" field with real content.
2. Crawl smoke test
curl -s -X POST https://api.fastcrw.com/v1/crawl \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "maxDepth": 1, "maxPages": 3}' \
| python3 -m json.tool
Expected: {"success": true, "id": "<job-id>"}. Poll GET /v1/crawl/{id} until "status": "completed".
3. Map smoke test
curl -s -X POST https://api.fastcrw.com/v1/map \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "maxDepth": 1}' \
| python3 -m json.tool | head -20
Expected: "links": [...] with discovered URLs.
4. Search smoke test
curl -s -X POST https://api.fastcrw.com/v1/search \
-H "Authorization: Bearer $CRW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "fastcrw web scraper", "limit": 3}' \
| python3 -m json.tool | head -30
Expected: "data": [...] with search results. (Against a self-hosted engine the same
call returns "data": {"results": [...]} — see Response shapes.)
5. SDK round-trip (Python)
import os
from crw import CrwClient
client = CrwClient() # CRW_API_KEY must be set
# Basic scrape
result = client.scrape("https://example.com", formats=["markdown"])
assert result.get("markdown"), "markdown field is empty"
print("scrape: OK")
# Map
links = client.map("https://example.com", max_depth=1)
assert isinstance(links, list) and len(links) > 0, "map returned no links"
print(f"map: OK ({len(links)} links)")
print("All checks passed — safe to promote to production.")
Feature-detect the engine
Use GET /v1/capabilities to confirm what the target engine supports before sending calls that depend on optional features (LLM extraction, file parsing, search):
curl -s https://api.fastcrw.com/v1/capabilities \
-H "Authorization: Bearer $CRW_API_KEY" \
| python3 -m json.tool
The response lists llm, search, documents.parsers, and formats so your code can branch instead of assuming.
What is not in scope
The following Firecrawl Cloud capabilities have no equivalent in fastCRW and are not planned:
/v1/deep-research(Spark model pipeline)/v1/agent(AI agent sessions)- Fire-engine proprietary anti-bot layer
Bring-your-own-proxy IS supported on a self-hosted instance: set a pool in [proxy], or pass proxy on the scrape body. See Proxies.
For any capability not in the matrix above, check the capability matrix which is the authoritative reference.