History

(python/js/ts-sdks): readmes, e2e tests w idmux etc all good

rafaelmmiller committed 1 year ago

Commit 537f6c41 parent f4d904631 files · +788 −226Browse files

Unified · Split

31 files
  1. apps/api/src/controllers/v2/extract-status.ts +94 −0
  2. apps/api/src/routes/v2.ts +16 −0
  3. apps/js-sdk/firecrawl/.env.example +4 −1
  4. apps/js-sdk/firecrawl/README.md +85 −78
  5. apps/js-sdk/firecrawl/src/__tests__/e2e/v2/batch.test.ts +8 −4
  6. apps/js-sdk/firecrawl/src/__tests__/e2e/v2/crawl.test.ts +8 −7
  7. apps/js-sdk/firecrawl/src/__tests__/e2e/v2/extract.test.ts +8 −4
  8. apps/js-sdk/firecrawl/src/__tests__/e2e/v2/map.test.ts +8 −7
  9. apps/js-sdk/firecrawl/src/__tests__/e2e/v2/scrape.test.ts +9 −8
  10. apps/js-sdk/firecrawl/src/__tests__/e2e/v2/search.test.ts +24 −14
  11. apps/js-sdk/firecrawl/src/__tests__/e2e/v2/usage.test.ts +8 −4
  12. apps/js-sdk/firecrawl/src/__tests__/e2e/v2/utils/idmux.ts +58 −0
  13. apps/js-sdk/firecrawl/src/__tests__/e2e/v2/watcher.test.ts +10 −4
  14. apps/js-sdk/firecrawl/src/index.ts +21 −7
  15. apps/js-sdk/firecrawl/src/v1/index.ts +12 −1
  16. apps/js-sdk/firecrawl/src/v2/client.ts +113 −0
  17. apps/js-sdk/firecrawl/src/v2/types.ts +3 −3
  18. apps/python-sdk/.env.example +4 −1
  19. apps/python-sdk/README.md +44 −28
  20. apps/python-sdk/firecrawl/__tests__/e2e/v2/.env.example +5 −2
  21. apps/python-sdk/firecrawl/__tests__/e2e/v2/aio/test_aio_map.py +0 −2
  22. apps/python-sdk/firecrawl/__tests__/e2e/v2/conftest.py +73 −0
  23. apps/python-sdk/firecrawl/__tests__/e2e/v2/test_map.py +1 −4
  24. apps/python-sdk/firecrawl/__tests__/e2e/v2/test_search.py +0 −6
  25. apps/python-sdk/firecrawl/__tests__/e2e/v2/test_usage.py +1 −6
  26. apps/python-sdk/firecrawl/__tests__/e2e/v2/test_watcher.py +1 −0
  27. apps/python-sdk/firecrawl/client.py +39 −17
  28. apps/python-sdk/firecrawl/firecrawl.backup.py +0 −0
  29. apps/python-sdk/firecrawl/v2/client.py +128 −14
  30. apps/python-sdk/firecrawl/v2/methods/aio/crawl.py +1 −2
  31. apps/python-sdk/firecrawl/v2/methods/aio/usage.py +2 −2
apps/api/src/controllers/v2/extract-status.ts+94−0
@@ -0,0 +1,94 @@
1+import { Response } from "express";
2+import { RequestWithAuth } from "./types";
3+import { getExtract, getExtractExpiry } from "../../lib/extract/extract-redis";
4+import { DBJob, PseudoJob } from "./crawl-status";
5+import { getExtractQueue } from "../../services/queue-service";
6+import { ExtractResult } from "../../lib/extract/extraction-service";
7+import { supabaseGetJobById } from "../../lib/supabase-jobs";
8+
9+export async function getExtractJob(id: string): Promise<PseudoJob<ExtractResult> | null> {
10+ const [bullJob, dbJob] = await Promise.all([
11+ getExtractQueue().getJob(id),
12+ (process.env.USE_DB_AUTHENTICATION === "true" ? supabaseGetJobById(id) : null) as Promise<DBJob | null>,
13+ ]);
14+
15+ if (!bullJob && !dbJob) return null;
16+
17+ const data = dbJob?.docs ?? bullJob?.returnvalue?.data;
18+
19+ const job: PseudoJob<any> = {
20+ id,
21+ getState: bullJob ? bullJob.getState.bind(bullJob) : (() => dbJob!.success ? "completed" : "failed"),
22+ returnvalue: data,
23+ data: {
24+ scrapeOptions: bullJob ? bullJob.data.scrapeOptions : dbJob!.page_options,
25+ teamId: bullJob ? bullJob.data.teamId : dbJob!.team_id,
26+ },
27+ timestamp: bullJob ? bullJob.timestamp : new Date(dbJob!.date_added).valueOf(),
28+ failedReason: (bullJob ? bullJob.failedReason : dbJob!.message) || undefined,
29+ }
30+
31+ return job;
32+}
33+
34+export async function extractStatusController(
35+ req: RequestWithAuth<{ jobId: string }, any, any>,
36+ res: Response,
37+) {
38+ const extract = await getExtract(req.params.jobId);
39+
40+ let status = extract?.status;
41+
42+ if (extract && extract.team_id !== req.auth.team_id) {
43+ return res.status(404).json({
44+ success: false,
45+ error: "Extract job not found",
46+ });
47+ }
48+
49+ let data: ExtractResult | [] = [];
50+
51+ if (!extract || extract.status === "completed") {
52+ const jobData = await getExtractJob(req.params.jobId);
53+ if ((!jobData && !extract) || (jobData && jobData.data.teamId !== req.auth.team_id)) {
54+ return res.status(404).json({
55+ success: false,
56+ error: "Extract job not found",
57+ });
58+ }
59+
60+ if (jobData) {
61+ const jobStatus = await jobData.getState();
62+
63+ if (jobStatus === "completed") {
64+ status = "completed";
65+ } else if (jobStatus === "failed") {
66+ status = "failed";
67+ } else {
68+ status = "processing";
69+ }
70+ }
71+
72+ if (!jobData?.returnvalue) {
73+ // if we got in the split-second where the redis is updated but the bull isn't
74+ // just pretend it's still processing - MG
75+ status = "processing";
76+ } else {
77+ data = jobData.returnvalue ?? [];
78+ }
79+ }
80+
81+ return res.status(200).json({
82+ success: status === "failed" ? false : true,
83+ data,
84+ status,
85+ error: extract?.error ?? undefined,
86+ expiresAt: (await getExtractExpiry(req.params.jobId)).toISOString(),
87+ steps: extract?.showSteps ? extract.steps : undefined,
88+ llmUsage: extract?.showLLMUsage ? extract.llmUsage : undefined,
89+ sources: extract?.showSources ? extract.sources : undefined,
90+ costTracking: extract?.showCostTracking ? extract.costTracking : undefined,
91+ sessionIds: extract?.sessionIds ? extract.sessionIds : undefined,
92+ tokensUsed: extract?.tokensBilled ? extract.tokensBilled : undefined,
93+ });
94+}
apps/api/src/routes/v2.ts+16−0
@@ -16,6 +16,8 @@ import { tokenUsageController } from "../controllers/v2/token-usage"; · 15 unmodified lines
1616 import { crawlCancelController } from "../controllers/v2/crawl-cancel";
1717 import { concurrencyCheckController } from "../controllers/v2/concurrency-check";
1818 import { crawlStatusWSController } from "../controllers/v2/crawl-status-ws";
19+import { extractController } from "../controllers/v2/extract";
20+import { extractStatusController } from "../controllers/v2/extract-status";
1921 import {
2022 authMiddleware,
2123 checkCreditsMiddleware,
@@ -122,6 +124,20 @@ v2Router.get( · 100 unmodified lines
122124 wrap(crawlErrorsController),
123125 );
124126
127+v2Router.post(
128+ "/extract",
129+ authMiddleware(RateLimiterMode.Extract),
130+ checkCreditsMiddleware(),
131+ wrap(extractController),
132+);
133+
134+v2Router.get(
135+ "/extract/:jobId",
136+ authMiddleware(RateLimiterMode.ExtractStatus),
137+ wrap(extractStatusController),
138+);
139+
140+
125141 v2Router.get(
126142 "/credit-usage",
127143 authMiddleware(RateLimiterMode.CrawlStatus),
apps/js-sdk/firecrawl/.env.example+4−1
@@ -1,2 +1,5 @@
11 FIRECRAWL_API_KEY=<your api key>
2−FIRECRAWL_API_URL=https://api.firecrawl.dev
2+FIRECRAWL_API_URL=https://api.firecrawl.dev
3+
4+# TESTING PURPOSE:
5+IDMUX_URL=
apps/js-sdk/firecrawl/README.md+85−78
@@ -18,171 +18,178 @@ npm install @mendable/firecrawl-js · 17 unmodified lines
1818 Here's an example of how to use the SDK with error handling:
1919
2020 ```js
21−import FirecrawlApp, { CrawlParams, CrawlStatusResponse } from '@mendable/firecrawl-js';
21+import Firecrawl from '@mendable/firecrawl-js';
2222
23−const app = new FirecrawlApp({apiKey: "fc-YOUR_API_KEY"});
23+const app = new Firecrawl({ apiKey: 'fc-YOUR_API_KEY' });
2424
2525 // Scrape a website
26−const scrapeResponse = await app.scrapeUrl('https://firecrawl.dev', {
26+const scrapeResponse = await app.scrape('https://firecrawl.dev', {
2727 formats: ['markdown', 'html'],
2828 });
29+console.log(scrapeResponse);
2930
30−if (scrapeResponse) {
31− console.log(scrapeResponse)
32−}
33−
34−// Crawl a website
35−const crawlResponse = await app.crawlUrl('https://firecrawl.dev', {
31+// Crawl a website (waiter)
32+const crawlResponse = await app.crawl('https://firecrawl.dev', {
3633 limit: 100,
37− scrapeOptions: {
38− formats: ['markdown', 'html'],
39− }
40−})
41−
42−console.log(crawlResponse)
34+ scrapeOptions: { formats: ['markdown', 'html'] },
35+ pollInterval: 2,
36+});
37+console.log(crawlResponse);
4338 ```
4439
4540 ### Scraping a URL
4641
47−To scrape a single URL with error handling, use the `scrapeUrl` method. It takes the URL as a parameter and returns the scraped data as a dictionary.
42+To scrape a single URL with error handling, use the `scrape` method. It takes the URL as a parameter and returns the scraped data.
4843
4944 ```js
50−const url = "https://example.com";
51−const scrapedData = await app.scrapeUrl(url);
45+const url = 'https://example.com';
46+const scrapedData = await app.scrape(url);
5247 ```
5348
5449 ### Crawling a Website
5550
56−To crawl a website with error handling, use the `crawlUrl` method. It takes the starting URL and optional parameters as arguments. The `params` argument allows you to specify additional options for the crawl job, such as the maximum number of pages to crawl, allowed domains, and the output format.
51+To crawl a website with error handling, use the `crawl` method. It takes the starting URL and optional parameters, including limits and per‑page `scrapeOptions`.
5752
5853 ```js
59−const crawlResponse = await app.crawlUrl('https://firecrawl.dev', {
54+const crawlResponse = await app.crawl('https://firecrawl.dev', {
6055 limit: 100,
61− scrapeOptions: {
62− formats: ['markdown', 'html'],
63− }
64−})
56+ scrapeOptions: { formats: ['markdown', 'html'] },
57+});
6558 ```
6659
6760
6861 ### Asynchronous Crawl
6962
70−To initiate an asynchronous crawl of a website, utilize the AsyncCrawlURL method. This method requires the starting URL and optional parameters as inputs. The params argument enables you to define various settings for the asynchronous crawl, such as the maximum number of pages to crawl, permitted domains, and the output format. Upon successful initiation, this method returns an ID, which is essential for subsequently checking the status of the crawl.
63+To start an asynchronous crawl, use `startCrawl`. It returns a job ID you can poll with `getCrawlStatus`.
7164
7265 ```js
73−const asyncCrawlResult = await app.asyncCrawlUrl('mendable.ai', { excludePaths: ['blog/*'], limit: 5});
66+const start = await app.startCrawl('https://mendable.ai', {
67+ excludePaths: ['blog/*'],
68+ limit: 5,
69+});
7470 ```
7571
7672 ### Checking Crawl Status
7773
78−To check the status of a crawl job with error handling, use the `checkCrawlStatus` method. It takes the job ID as a parameter and returns the current status of the crawl job`
74+To check the status of a crawl job with error handling, use the `getCrawlStatus` method. It takes the job ID as a parameter and returns the current status.
7975
8076 ```js
81−const status = await app.checkCrawlStatus(id);
77+const status = await app.getCrawlStatus(id);
8278 ```
8379
84−### Extracting structured data from a URL
80+### Extracting structured data from URLs
8581
86−With LLM extraction, you can easily extract structured data from any URL. We support zod schema to make it easier for you too. Here is how you to use it:
82+Use `extract` with a prompt and schema. Zod schemas are supported directly.
8783
8884 ```js
89−import FirecrawlApp from "@mendable/firecrawl-js";
90−import { z } from "zod";
85+import Firecrawl from '@mendable/firecrawl-js';
86+import { z } from 'zod';
9187
92−const app = new FirecrawlApp({
93− apiKey: "fc-YOUR_API_KEY",
94−});
88+const app = new Firecrawl({ apiKey: 'fc-YOUR_API_KEY' });
9589
96−// Define schema to extract contents into
9790 const schema = z.object({
98− top: z
99− .array(
100− z.object({
101− title: z.string(),
102− points: z.number(),
103− by: z.string(),
104− commentsURL: z.string(),
105− })
106− )
107− .length(5)
108− .describe("Top 5 stories on Hacker News"),
91+ title: z.string(),
10992 });
11093
111−const scrapeResult = await app.scrapeUrl("https://firecrawl.dev", {
112− extractorOptions: { extractionSchema: schema },
94+const result = await app.extract({
95+ urls: ['https://firecrawl.dev'],
96+ prompt: 'Extract the page title',
97+ schema,
98+ showSources: true,
11399 });
114100
115−console.log(scrapeResult.data["llm_extraction"]);
101+console.log(result.data);
116102 ```
117103
118104 ### Map a Website
119105
120−Use `map_url` to generate a list of URLs from a website. The `params` argument let you customize the mapping process, including options to exclude subdomains or to utilize the sitemap.
106+Use `map` to generate a list of URLs from a website. Options let you customize the mapping process, including whether to utilize the sitemap or include subdomains.
121107
122108 ```js
123−const mapResult = await app.mapUrl('https://example.com') as MapResponse;
124−console.log(mapResult)
109+const mapResult = await app.map('https://example.com');
110+console.log(mapResult);
125111 ```
126112
127−### Crawl a website with WebSockets
113+### Crawl a website with real‑time updates
128114
129−To crawl a website with WebSockets, use the `crawlUrlAndWatch` method. It takes the starting URL and optional parameters as arguments. The `params` argument allows you to specify additional options for the crawl job, such as the maximum number of pages to crawl, allowed domains, and the output format.
115+To receive real‑time updates, start a crawl and attach a watcher.
130116
131117 ```js
132−// Crawl a website with WebSockets:
133−const watch = await app.crawlUrlAndWatch('mendable.ai', { excludePaths: ['blog/*'], limit: 5});
118+const start = await app.startCrawl('https://mendable.ai', { excludePaths: ['blog/*'], limit: 5 });
119+const watch = app.watcher(start.id, { kind: 'crawl', pollInterval: 2 });
134120
135−watch.addEventListener("document", doc => {
136− console.log("DOC", doc.detail);
121+watch.on('document', (doc) => {
122+ console.log('DOC', doc);
137123 });
138124
139−watch.addEventListener("error", err => {
140− console.error("ERR", err.detail.error);
125+watch.on('error', (err) => {
126+ console.error('ERR', err);
141127 });
142128
143−watch.addEventListener("done", state => {
144− console.log("DONE", state.detail.status);
129+watch.on('done', (state) => {
130+ console.log('DONE', state.status);
145131 });
132+
133+await watch.start();
146134 ```
147135
148136 ### Batch scraping multiple URLs
149137
150−To batch scrape multiple URLs with error handling, use the `batchScrapeUrls` method. It takes the starting URLs and optional parameters as arguments. The `params` argument allows you to specify additional options for the batch scrape job, such as the output formats.
138+To batch scrape multiple URLs with error handling, use the `batchScrape` method.
151139
152140 ```js
153−const batchScrapeResponse = await app.batchScrapeUrls(['https://firecrawl.dev', 'https://mendable.ai'], {
141+const batchScrapeResponse = await app.batchScrape(['https://firecrawl.dev', 'https://mendable.ai'], {
154142 formats: ['markdown', 'html'],
155−})
143+});
156144 ```
157145
158146
159147 #### Asynchronous batch scrape
160148
161−To initiate an asynchronous batch scrape, utilize the `asyncBatchScrapeUrls` method. This method requires the starting URLs and optional parameters as inputs. The params argument enables you to define various settings for the scrape, such as the output formats. Upon successful initiation, this method returns an ID, which is essential for subsequently checking the status of the batch scrape.
149+To start an asynchronous batch scrape, use `startBatchScrape` and poll with `getBatchScrapeStatus`.
162150
163151 ```js
164−const asyncBatchScrapeResult = await app.asyncBatchScrapeUrls(['https://firecrawl.dev', 'https://mendable.ai'], { formats: ['markdown', 'html'] });
152+const asyncBatchScrapeResult = await app.startBatchScrape(['https://firecrawl.dev', 'https://mendable.ai'], {
153+ formats: ['markdown', 'html'],
154+});
165155 ```
166156
167−#### Batch scrape with WebSockets
157+#### Batch scrape with real‑time updates
168158
169−To use batch scrape with WebSockets, use the `batchScrapeUrlsAndWatch` method. It takes the starting URL and optional parameters as arguments. The `params` argument allows you to specify additional options for the batch scrape job, such as the output formats.
159+To use batch scrape with real‑time updates, start the job and watch it using the watcher.
170160
171161 ```js
172−// Batch scrape multiple URLs with WebSockets:
173−const watch = await app.batchScrapeUrlsAndWatch(['https://firecrawl.dev', 'https://mendable.ai'], { formats: ['markdown', 'html'] });
162+const start = await app.startBatchScrape(['https://firecrawl.dev', 'https://mendable.ai'], { formats: ['markdown', 'html'] });
163+const watch = app.watcher(start.id, { kind: 'batch', pollInterval: 2 });
174164
175−watch.addEventListener("document", doc => {
176− console.log("DOC", doc.detail);
165+watch.on('document', (doc) => {
166+ console.log('DOC', doc);
177167 });
178168
179−watch.addEventListener("error", err => {
180− console.error("ERR", err.detail.error);
169+watch.on('error', (err) => {
170+ console.error('ERR', err);
181171 });
182172
183−watch.addEventListener("done", state => {
184− console.log("DONE", state.detail.status);
173+watch.on('done', (state) => {
174+ console.log('DONE', state.status);
185175 });
176+
177+await watch.start();
178+```
179+
180+## v1 compatibility
181+
182+The feature‑frozen v1 is still available under `app.v1` with the original method names.
183+
184+```js
185+import Firecrawl from '@mendable/firecrawl-js';
186+
187+const app = new Firecrawl({ apiKey: 'fc-YOUR_API_KEY' });
188+
189+// v1 methods (feature‑frozen)
190+const scrapeV1 = await app.v1.scrapeUrl('https://firecrawl.dev', { formats: ['markdown', 'html'] });
191+const crawlV1 = await app.v1.crawlUrl('https://firecrawl.dev', { limit: 100 });
192+const mapV1 = await app.v1.mapUrl('https://firecrawl.dev');
186193 ```
187194
188195 ## Error Handling
apps/js-sdk/firecrawl/src/__tests__/e2e/v2/batch.test.ts+8−4
@@ -3,14 +3,18 @@ · 2 unmodified lines
33 */
44 import Firecrawl from "../../../index";
55 import { config } from "dotenv";
6−import { describe, test, expect } from "@jest/globals";
6+import { getIdentity, getApiUrl } from "./utils/idmux";
7+import { describe, test, expect, beforeAll } from "@jest/globals";
78
89 config();
910
10−const API_KEY = process.env.FIRECRAWL_API_KEY ?? "";
11−const API_URL = process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev";
11+const API_URL = getApiUrl();
12+let client: Firecrawl;
1213
13−const client = new Firecrawl({ apiKey: API_KEY, apiUrl: API_URL });
14+beforeAll(async () => {
15+ const { apiKey } = await getIdentity({ name: "js-e2e-batch" });
16+ client = new Firecrawl({ apiKey, apiUrl: API_URL });
17+});
1418
1519 describe("v2.batch e2e", () => {
1620 test("batch scrape minimal (wait)", async () => {
apps/js-sdk/firecrawl/src/__tests__/e2e/v2/crawl.test.ts+8−7
@@ -3,19 +3,20 @@ · 2 unmodified lines
33 */
44 import Firecrawl from "../../../index";
55 import { config } from "dotenv";
6−import { describe, test, expect } from "@jest/globals";
6+import { getIdentity, getApiUrl } from "./utils/idmux";
7+import { describe, test, expect, beforeAll } from "@jest/globals";
78
89 config();
910
10−const API_KEY = process.env.FIRECRAWL_API_KEY ?? "";
11−const API_URL = process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev";
11+const API_URL = getApiUrl();
12+let client: Firecrawl;
1213
13−const client = new Firecrawl({ apiKey: API_KEY, apiUrl: API_URL });
14+beforeAll(async () => {
15+ const { apiKey } = await getIdentity({ name: "js-e2e-crawl" });
16+ client = new Firecrawl({ apiKey, apiUrl: API_URL });
17+});
1418
1519 describe("v2.crawl e2e", () => {
16− if (!client) {
17− console.warn("Skipping v2.crawl e2e: FIRECRAWL_API_KEY not set");
18− }
1920
2021 test("start crawl minimal request", async () => {
2122 if (!client) throw new Error();
apps/js-sdk/firecrawl/src/__tests__/e2e/v2/extract.test.ts+8−4
@@ -3,15 +3,19 @@ · 2 unmodified lines
33 */
44 import Firecrawl from "../../../index";
55 import { config } from "dotenv";
6−import { describe, test, expect } from "@jest/globals";
6+import { getIdentity, getApiUrl } from "./utils/idmux";
7+import { describe, test, expect, beforeAll } from "@jest/globals";
78 import { z } from "zod";
89
910 config();
1011
11−const API_KEY = process.env.FIRECRAWL_API_KEY ?? "";
12−const API_URL = process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev";
12+const API_URL = getApiUrl();
13+let client: Firecrawl;
1314
14−const client = new Firecrawl({ apiKey: API_KEY, apiUrl: API_URL });
15+beforeAll(async () => {
16+ const { apiKey } = await getIdentity({ name: "js-e2e-extract" });
17+ client = new Firecrawl({ apiKey, apiUrl: API_URL });
18+});
1519
1620 describe("v2.extract e2e", () => {
1721 test("extract minimal with prompt", async () => {
apps/js-sdk/firecrawl/src/__tests__/e2e/v2/map.test.ts+8−7
@@ -3,19 +3,20 @@ · 2 unmodified lines
33 */
44 import Firecrawl from "../../../index";
55 import { config } from "dotenv";
6−import { describe, test, expect } from "@jest/globals";
6+import { getIdentity, getApiUrl } from "./utils/idmux";
7+import { describe, test, expect, beforeAll } from "@jest/globals";
78
89 config();
910
10−const API_KEY = process.env.FIRECRAWL_API_KEY ?? "";
11−const API_URL = process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev";
11+const API_URL = getApiUrl();
12+let client: Firecrawl;
1213
13−const client = API_KEY ? new Firecrawl({ apiKey: API_KEY, apiUrl: API_URL }) : null as any;
14+beforeAll(async () => {
15+ const { apiKey } = await getIdentity({ name: "js-e2e-map" });
16+ client = new Firecrawl({ apiKey, apiUrl: API_URL });
17+});
1418
1519 describe("v2.map e2e", () => {
16− if (!client) {
17− console.warn("Skipping v2.map e2e: FIRECRAWL_API_KEY not set");
18− }
1920
2021 test("minimal request", async () => {
2122 if (!client) throw new Error();
apps/js-sdk/firecrawl/src/__tests__/e2e/v2/scrape.test.ts+9−8
@@ -4,19 +4,20 @@ · 3 unmodified lines
44 import Firecrawl from "../../../index";
55 import { z } from "zod";
66 import { config } from "dotenv";
7−import { describe, test, expect } from "@jest/globals";
7+import { getIdentity, getApiUrl } from "./utils/idmux";
8+import { describe, test, expect, beforeAll } from "@jest/globals";
89
910 config();
1011
11−const API_KEY = process.env.FIRECRAWL_API_KEY ?? "";
12−const API_URL = process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev";
12+const API_URL = getApiUrl();
13+let client: Firecrawl;
1314
14−describe("v2.scrape e2e", () => {
15− if (!API_KEY) {
16− console.warn("Skipping v2.scrape e2e: FIRECRAWL_API_KEY not set");
17− }
15+beforeAll(async () => {
16+ const { apiKey } = await getIdentity({ name: "js-e2e-scrape" });
17+ client = new Firecrawl({ apiKey, apiUrl: API_URL });
18+});
1819
19− const client = new Firecrawl({ apiKey: API_KEY, apiUrl: API_URL })
20+describe("v2.scrape e2e", () => {
2021
2122 const assertValidDocument = (doc: any) => {
2223 expect(doc).toBeTruthy();
apps/js-sdk/firecrawl/src/__tests__/e2e/v2/search.test.ts+24−14
@@ -2,15 +2,20 @@ · 1 unmodified line
22 * E2E tests for v2 search (translated from Python tests)
33 */
44 import Firecrawl from "../../../index";
5+import type { Document, SearchResult } from "../../../index";
56 import { config } from "dotenv";
6−import { describe, test, expect } from "@jest/globals";
7+import { getIdentity, getApiUrl } from "./utils/idmux";
8+import { describe, test, expect, beforeAll } from "@jest/globals";
79
810 config();
911
10−const API_KEY = process.env.FIRECRAWL_API_KEY ?? "";
11−const API_URL = process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev";
12+const API_URL = getApiUrl();
13+let client: Firecrawl;
1214
13−const client = API_KEY ? new Firecrawl({ apiKey: API_KEY, apiUrl: API_URL }) : null as any;
15+beforeAll(async () => {
16+ const { apiKey } = await getIdentity({ name: "js-e2e-search" });
17+ client = new Firecrawl({ apiKey, apiUrl: API_URL });
18+});
1419
1520 function collectTexts(entries: any[] | undefined): string[] {
1621 const texts: string[] = [];
@@ -23,14 +28,13 @@ function collectTexts(entries: any[] | undefined): string[] { · 6 unmodified lines
2328 return texts;
2429 }
2530
26−function isDocument(entry: any): boolean {
27− return !!entry && (typeof entry.markdown === 'string' || typeof entry.html === 'string');
31+function isDocument(entry: Document | SearchResult | undefined | null): entry is Document {
32+ if (!entry) return false;
33+ const d = entry as Document;
34+ return typeof d.markdown === 'string' || typeof d.html === 'string';
2835 }
2936
3037 describe("v2.search e2e", () => {
31− if (!client) {
32− console.warn("Skipping v2.search e2e: FIRECRAWL_API_KEY not set");
33− }
3438
3539 test("minimal request", async () => {
3640 if (!client) throw new Error();
@@ -72,8 +76,12 @@ describe("v2.search e2e", () => { · 35 unmodified lines
7276 }
7377 expect(results.images == null).toBe(true);
7478
75− const webTitles = (results.web || []).map(r => (r?.title || "").toString().toLowerCase());
76− const webDescriptions = (results.web || []).map(r => (r?.description || "").toString().toLowerCase());
79+ const webTitles = (results.web || [])
80+ .filter((r): r is SearchResult => !isDocument(r))
81+ .map(r => (r.title || "").toString().toLowerCase());
82+ const webDescriptions = (results.web || [])
83+ .filter((r): r is SearchResult => !isDocument(r))
84+ .map(r => (r.description || "").toString().toLowerCase());
7785 const allWebText = (webTitles.concat(webDescriptions)).join(" ");
7886 expect(allWebText.includes("firecrawl")).toBe(true);
7987 }, 90_000);
@@ -174,8 +182,10 @@ describe("v2.search e2e", () => { · 94 unmodified lines
174182 expect(results.images).toBeTruthy();
175183 expect((results.images || []).length).toBeLessThanOrEqual(3);
176184 for (const result of results.images || []) {
177− expect(typeof result.url).toBe("string");
178− expect(result.url.startsWith("http")).toBe(true);
185+ if (!isDocument(result)) {
186+ expect(typeof result.url).toBe("string");
187+ expect(result.url.startsWith("http")).toBe(true);
188+ }
179189 }
180190 }, 120_000);
181191
@@ -187,7 +197,7 @@ describe("v2.search e2e", () => { · 5 unmodified lines
187197 });
188198 const results2 = await client.search("python programming", {
189199 limit: 1,
190− scrapeOptions: { formats: [{ type: "markdown" as any }] },
200+ scrapeOptions: { formats: ["markdown"] },
191201 });
192202 expect(results1).toBeTruthy();
193203 expect(results2).toBeTruthy();
apps/js-sdk/firecrawl/src/__tests__/e2e/v2/usage.test.ts+8−4
@@ -3,14 +3,18 @@ · 2 unmodified lines
33 */
44 import Firecrawl from "../../../index";
55 import { config } from "dotenv";
6−import { describe, test, expect } from "@jest/globals";
6+import { getIdentity, getApiUrl } from "./utils/idmux";
7+import { describe, test, expect, beforeAll } from "@jest/globals";
78
89 config();
910
10−const API_KEY = process.env.FIRECRAWL_API_KEY ?? "";
11−const API_URL = process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev";
11+const API_URL = getApiUrl();
12+let client: Firecrawl;
1213
13−const client = new Firecrawl({ apiKey: API_KEY, apiUrl: API_URL });
14+beforeAll(async () => {
15+ const { apiKey } = await getIdentity({ name: "js-e2e-usage" });
16+ client = new Firecrawl({ apiKey, apiUrl: API_URL });
17+});
1418
1519 describe("v2.usage e2e", () => {
1620 test("get_concurrency", async () => {
apps/js-sdk/firecrawl/src/__tests__/e2e/v2/utils/idmux.ts+58−0
@@ -0,0 +1,58 @@
1+export type IdmuxRequest = {
2+ name?: string;
3+ concurrency?: number;
4+ credits?: number;
5+ tokens?: number;
6+ teamId?: string;
7+ flags?: Record<string, unknown>;
8+};
9+
10+export type Identity = {
11+ apiKey: string;
12+ teamId: string;
13+};
14+
15+let cachedIdentity: Identity | null = null;
16+
17+export function getApiUrl(): string {
18+ return process.env.TEST_URL ?? process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev";
19+}
20+
21+export async function getIdentity(req: IdmuxRequest = {}): Promise<Identity> {
22+ if (cachedIdentity) return cachedIdentity;
23+
24+ const idmuxUrl = process.env.IDMUX_URL;
25+ if (!idmuxUrl) {
26+ const fallback: Identity = {
27+ apiKey: process.env.TEST_API_KEY ?? process.env.FIRECRAWL_API_KEY ?? "",
28+ teamId: process.env.TEST_TEAM_ID ?? "",
29+ };
30+ cachedIdentity = fallback;
31+ return fallback;
32+ }
33+
34+ const runNumberRaw = process.env.GITHUB_RUN_NUMBER;
35+ const runNumber = runNumberRaw ? Number(runNumberRaw) : 0;
36+ const body = {
37+ refName: process.env.GITHUB_REF_NAME ?? "local",
38+ runNumber: Number.isFinite(runNumber) ? runNumber : 0,
39+ concurrency: req.concurrency ?? 100,
40+ ...req,
41+ };
42+
43+ const res = await fetch(`${idmuxUrl}/`, {
44+ method: "POST",
45+ headers: { "Content-Type": "application/json" },
46+ body: JSON.stringify(body),
47+ });
48+
49+ if (!res.ok) {
50+ const text = await res.text();
51+ throw new Error(`idmux request failed: ${res.status} ${text}`);
52+ }
53+
54+ const identity = (await res.json()) as Identity;
55+ cachedIdentity = identity;
56+ return identity;
57+}
58+
apps/js-sdk/firecrawl/src/__tests__/e2e/v2/watcher.test.ts+10−4
@@ -1,15 +1,21 @@
11 import Firecrawl from "../../../index";
22 import { config } from "dotenv";
3−import { describe, test, expect } from "@jest/globals";
3+import { describe, test, expect, beforeAll } from "@jest/globals";
4+import { getIdentity } from "./utils/idmux";
45
56 config();
67
7−const API_KEY = process.env.FIRECRAWL_API_KEY ?? "";
88 const API_URL = process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev";
9+let client: Firecrawl;
10+
11+beforeAll(async () => {
12+ const { apiKey } = await getIdentity({ name: "js-e2e-watcher" });
13+ client = new Firecrawl({ apiKey, apiUrl: API_URL });
14+});
915
1016 describe("v2.watcher e2e", () => {
1117 test("crawl watcher minimal", async () => {
12− const client = new Firecrawl({ apiKey: API_KEY, apiUrl: API_URL });
18+ // client is initialized in beforeAll
1319 const start = await client.startCrawl("https://docs.firecrawl.dev", { limit: 3 });
1420
1521 expect(typeof start.id).toBe("string");
@@ -49,7 +55,7 @@ describe("v2.watcher e2e", () => { · 33 unmodified lines
4955 }, 240_000);
5056
5157 test("batch watcher with options (kind, pollInterval, timeout)", async () => {
52− const client = new Firecrawl({ apiKey: API_KEY, apiUrl: API_URL });
58+ // client is initialized in beforeAll
5359 const urls = [
5460 "https://docs.firecrawl.dev",
5561 "https://firecrawl.dev",
apps/js-sdk/firecrawl/src/index.ts+21−7
@@ -1,23 +1,37 @@
1−// Unified entrypoint for Firecrawl JS SDK
2−// - Default export: unified Firecrawl (v2 by default, v1 under .v1)
3−// - Named export FirecrawlClient: direct v2 client
4−// - Named export FirecrawlAppV1: legacy v1 class
5−// - Re-export v2 types
1+/**
2+ * Firecrawl JS/TS SDK — unified entrypoint.
3+ * - v2 by default on the top‑level client
4+ * - v1 available under `.v1` (feature‑frozen)
5+ * - Exports: `Firecrawl` (default), `FirecrawlClient` (v2), `FirecrawlAppV1` (v1), and v2 types
6+ */
67
8+/** Direct v2 client. */
79 export { FirecrawlClient } from "./v2/client";
10+/** Public v2 request/response types. */
811 export * from "./v2/types";
12+/** Legacy v1 client (feature‑frozen). */
913 export { default as FirecrawlAppV1 } from "./v1";
1014
1115 import V1 from "./v1";
1216 import { FirecrawlClient as V2 } from "./v2/client";
1317 import type { FirecrawlAppConfig } from "./v1";
1418
19+/** Unified client: extends v2 and adds `.v1` for backward compatibility. */
1520 export class Firecrawl extends V2 {
16− public v1: V1;
21+ /** Feature‑frozen v1 client (lazy). */
22+ private _v1?: V1;
23+ private _v1Opts: FirecrawlAppConfig;
1724
25+ /** @param opts API credentials and base URL. */
1826 constructor(opts: FirecrawlAppConfig = {}) {
1927 super(opts as any);
20− this.v1 = new V1(opts);
28+ this._v1Opts = opts;
29+ }
30+
31+ /** Access the legacy v1 client (instantiated on first access). */
32+ get v1(): V1 {
33+ if (!this._v1) this._v1 = new V1(this._v1Opts);
34+ return this._v1;
2135 }
2236 }
2337
apps/js-sdk/firecrawl/src/v1/index.ts+12−1
@@ -582,10 +582,21 @@ export default class FirecrawlApp { · 581 unmodified lines
582582
583583 private async getVersion(): Promise<string> {
584584 try {
585+ // Prefer env-provided version when available (works well in test runners)
586+ if (typeof process !== 'undefined' && process.env && process.env.npm_package_version) {
587+ return process.env.npm_package_version as string;
588+ }
585589 const packageJson = await import('../../package.json', { assert: { type: 'json' } });
586590 return packageJson.default.version;
587591 } catch (error) {
588− console.error("Error getting version:", error);
592+ // Suppress noisy logs under test environments
593+ const isTest = typeof process !== 'undefined' && (
594+ process.env.JEST_WORKER_ID != null || process.env.NODE_ENV === 'test'
595+ );
596+ if (!isTest) {
597+ // eslint-disable-next-line no-console
598+ console.error("Error getting version:", error);
599+ }
589600 return "1.25.1";
590601 }
591602 }
apps/js-sdk/firecrawl/src/v2/client.ts+113−0
@@ -38,17 +38,32 @@ import type { · 37 unmodified lines
3838 import { Watcher } from "./watcher";
3939 import type { WatcherOptions } from "./watcher";
4040
41+/**
42+ * Configuration for the v2 client transport.
43+ */
4144 export interface FirecrawlClientOptions {
45+ /** API key (falls back to FIRECRAWL_API_KEY). */
4246 apiKey?: string | null;
47+ /** API base URL (falls back to FIRECRAWL_API_URL or https://api.firecrawl.dev). */
4348 apiUrl?: string | null;
49+ /** Per-request timeout in milliseconds (optional). */
4450 timeoutMs?: number;
51+ /** Max automatic retries for transient failures (optional). */
4552 maxRetries?: number;
53+ /** Exponential backoff factor for retries (optional). */
4654 backoffFactor?: number;
4755 }
4856
57+/**
58+ * Firecrawl v2 client. Provides typed access to all v2 endpoints and utilities.
59+ */
4960 export class FirecrawlClient {
5061 private readonly http: HttpClient;
5162
63+ /**
64+ * Create a v2 client.
65+ * @param options Transport configuration (API key, base URL, timeouts, retries).
66+ */
5267 constructor(options: FirecrawlClientOptions = {}) {
5368 const apiKey = options.apiKey ?? process.env.FIRECRAWL_API_KEY ?? "";
5469 const apiUrl = (options.apiUrl ?? process.env.FIRECRAWL_API_URL ?? "https://api.firecrawl.dev").replace(/\/$/, "");
@@ -65,83 +80,181 @@ export class FirecrawlClient { · 10 unmodified lines
6580 }
6681
6782 // Scrape
83+ /**
84+ * Scrape a single URL.
85+ * @param url Target URL.
86+ * @param options Optional scrape options (formats, headers, etc.).
87+ * @returns Resolved document with requested formats.
88+ */
6889 async scrape(url: string, options?: ScrapeOptions): Promise<Document> {
6990 return scrape(this.http, url, options);
7091 }
7192
7293 // Search
94+ /**
95+ * Search the web and optionally scrape each result.
96+ * @param query Search query string.
97+ * @param req Additional search options (sources, limit, scrapeOptions, etc.).
98+ * @returns Structured search results.
99+ */
73100 async search(query: string, req: Omit<SearchRequest, "query"> = {}): Promise<SearchData> {
74101 return search(this.http, { query, ...req });
75102 }
76103
77104 // Map
105+ /**
106+ * Map a site to discover URLs (sitemap-aware).
107+ * @param url Root URL to map.
108+ * @param options Mapping options (sitemap mode, includeSubdomains, limit, timeout).
109+ * @returns Discovered links.
110+ */
78111 async map(url: string, options?: MapOptions): Promise<MapData> {
79112 return mapMethod(this.http, url, options);
80113 }
81114
82115 // Crawl
116+ /**
117+ * Start a crawl job (async).
118+ * @param url Root URL to crawl.
119+ * @param req Crawl configuration (paths, limits, scrapeOptions, webhook, etc.).
120+ * @returns Job id and url.
121+ */
83122 async startCrawl(url: string, req: Omit<Parameters<typeof startCrawl>[1], "url"> = {}): Promise<CrawlResponse> {
84123 return startCrawl(this.http, { url, ...(req as any) });
85124 }
125+ /**
126+ * Get the status and partial data of a crawl job.
127+ * @param jobId Crawl job id.
128+ */
86129 async getCrawlStatus(jobId: string): Promise<CrawlJob> {
87130 return getCrawlStatus(this.http, jobId);
88131 }
132+ /**
133+ * Cancel a crawl job.
134+ * @param jobId Crawl job id.
135+ * @returns True if cancelled.
136+ */
89137 async cancelCrawl(jobId: string): Promise<boolean> {
90138 return cancelCrawl(this.http, jobId);
91139 }
140+ /**
141+ * Convenience waiter: start a crawl and poll until it finishes.
142+ * @param url Root URL to crawl.
143+ * @param req Crawl configuration plus waiter controls (pollInterval, timeout seconds).
144+ * @returns Final job snapshot.
145+ */
92146 async crawl(url: string, req: Omit<Parameters<typeof startCrawl>[1], "url"> & { pollInterval?: number; timeout?: number } = {}): Promise<CrawlJob> {
93147 return crawlWaiter(this.http, { url, ...(req as any) }, req.pollInterval, req.timeout);
94148 }
149+ /**
150+ * Retrieve crawl errors and robots.txt blocks.
151+ * @param crawlId Crawl job id.
152+ */
95153 async getCrawlErrors(crawlId: string): Promise<CrawlErrorsResponse> {
96154 return getCrawlErrors(this.http, crawlId);
97155 }
156+ /**
157+ * List active crawls for the authenticated team.
158+ */
98159 async getActiveCrawls(): Promise<ActiveCrawlsResponse> {
99160 return getActiveCrawls(this.http);
100161 }
162+ /**
163+ * Preview normalized crawl parameters produced by a natural-language prompt.
164+ * @param url Root URL.
165+ * @param prompt Natural-language instruction.
166+ */
101167 async crawlParamsPreview(url: string, prompt: string): Promise<Record<string, unknown>> {
102168 return crawlParamsPreview(this.http, url, prompt);
103169 }
104170
105171 // Batch
172+ /**
173+ * Start a batch scrape job for multiple URLs (async).
174+ * @param urls URLs to scrape.
175+ * @param opts Batch options (scrape options, webhook, concurrency, idempotency key, etc.).
176+ * @returns Job id and url.
177+ */
106178 async startBatchScrape(urls: string[], opts?: Parameters<typeof startBatchScrape>[2]): Promise<BatchScrapeResponse> {
107179 return startBatchScrape(this.http, urls, opts);
108180 }
181+ /**
182+ * Get the status and partial data of a batch scrape job.
183+ * @param jobId Batch job id.
184+ */
109185 async getBatchScrapeStatus(jobId: string): Promise<BatchScrapeJob> {
110186 return getBatchScrapeStatus(this.http, jobId);
111187 }
188+ /**
189+ * Retrieve batch scrape errors and robots.txt blocks.
190+ * @param jobId Batch job id.
191+ */
112192 async getBatchScrapeErrors(jobId: string): Promise<CrawlErrorsResponse> {
113193 return getBatchScrapeErrors(this.http, jobId);
114194 }
195+ /**
196+ * Cancel a batch scrape job.
197+ * @param jobId Batch job id.
198+ * @returns True if cancelled.
199+ */
115200 async cancelBatchScrape(jobId: string): Promise<boolean> {
116201 return cancelBatchScrape(this.http, jobId);
117202 }
203+ /**
204+ * Convenience waiter: start a batch scrape and poll until it finishes.
205+ * @param urls URLs to scrape.
206+ * @param opts Batch options plus waiter controls (pollInterval, timeout seconds).
207+ * @returns Final job snapshot.
208+ */
118209 async batchScrape(urls: string[], opts?: Parameters<typeof startBatchScrape>[2] & { pollInterval?: number; timeout?: number }): Promise<BatchScrapeJob> {
119210 return batchWaiter(this.http, urls, opts);
120211 }
121212
122213 // Extract
214+ /**
215+ * Start an extract job (async).
216+ * @param args Extraction request (urls, schema or prompt, flags).
217+ * @returns Job id or processing state.
218+ */
123219 async startExtract(args: Parameters<typeof startExtract>[1]): Promise<ExtractResponse> {
124220 return startExtract(this.http, args);
125221 }
222+ /**
223+ * Get extract job status/data.
224+ * @param jobId Extract job id.
225+ */
126226 async getExtractStatus(jobId: string): Promise<ExtractResponse> {
127227 return getExtractStatus(this.http, jobId);
128228 }
229+ /**
230+ * Convenience waiter: start an extract and poll until it finishes.
231+ * @param args Extraction request plus waiter controls (pollInterval, timeout seconds).
232+ * @returns Final extract response.
233+ */
129234 async extract(args: Parameters<typeof startExtract>[1] & { pollInterval?: number; timeout?: number }): Promise<ExtractResponse> {
130235 return extractWaiter(this.http, args);
131236 }
132237
133238 // Usage
239+ /** Current concurrency usage. */
134240 async getConcurrency() {
135241 return getConcurrency(this.http);
136242 }
243+ /** Current credit usage. */
137244 async getCreditUsage() {
138245 return getCreditUsage(this.http);
139246 }
247+ /** Recent token usage. */
140248 async getTokenUsage() {
141249 return getTokenUsage(this.http);
142250 }
143251
144252 // Watcher
253+ /**
254+ * Create a watcher for a crawl or batch job. Emits: `document`, `snapshot`, `done`, `error`.
255+ * @param jobId Job id.
256+ * @param opts Watcher options (kind, pollInterval, timeout seconds).
257+ */
145258 watcher(jobId: string, opts: WatcherOptions = {}): Watcher {
146259 return new Watcher(this.http, jobId, opts);
147260 }
apps/js-sdk/firecrawl/src/v2/types.ts+3−3
@@ -179,9 +179,9 @@ export interface SearchResult { · 178 unmodified lines
179179 }
180180
181181 export interface SearchData {
182− web?: Array<SearchResult>;
183− news?: Array<SearchResult>;
184− images?: Array<SearchResult>;
182+ web?: Array<SearchResult | Document>;
183+ news?: Array<SearchResult | Document>;
184+ images?: Array<SearchResult | Document>;
185185 }
186186
187187 export interface SearchRequest {
apps/python-sdk/.env.example+4−1
@@ -1,2 +1,5 @@
11 FIRECRAWL_API_KEY=<your api key>
2−FIRECRAWL_API_URL=https://api.firecrawl.dev
2+FIRECRAWL_API_URL=https://api.firecrawl.dev
3+
4+# TESTING PURPOSE:
5+IDMUX_URL=
apps/python-sdk/README.md+44−28
@@ -13,24 +13,25 @@ pip install firecrawl-py · 12 unmodified lines
1313 ## Usage
1414
1515 1. Get an API key from [firecrawl.dev](https://firecrawl.dev)
16−2. Set the API key as an environment variable named `FIRECRAWL_API_KEY` or pass it as a parameter to the `FirecrawlApp` class.
16+2. Set the API key as an environment variable named `FIRECRAWL_API_KEY` or pass it as a parameter to the `Firecrawl` class.
1717
1818 Here's an example of how to use the SDK:
1919
2020 ```python
21−from firecrawl import FirecrawlApp, ScrapeOptions
21+from firecrawl import Firecrawl
22+from firecrawl.types import ScrapeOptions
2223
23−app = FirecrawlApp(api_key="fc-YOUR_API_KEY")
24+firecrawl = Firecrawl(api_key="fc-YOUR_API_KEY")
2425
25−# Scrape a website:
26−data = app.scrape_url(
26+# Scrape a website (v2):
27+data = firecrawl.scrape(
2728 'https://firecrawl.dev',
2829 formats=['markdown', 'html']
2930 )
3031 print(data)
3132
32−# Crawl a website:
33−crawl_status = app.crawl_url(
33+# Crawl a website (v2 waiter):
34+crawl_status = firecrawl.crawl(
3435 'https://firecrawl.dev',
3536 limit=100,
3637 scrape_options=ScrapeOptions(formats=['markdown', 'html'])
@@ -40,20 +41,20 @@ print(crawl_status) · 3 unmodified lines
4041
4142 ### Scraping a URL
4243
43−To scrape a single URL, use the `scrape_url` method. It takes the URL as a parameter and returns the scraped data as a dictionary.
44+To scrape a single URL, use the `scrape` method. It takes the URL as a parameter and returns a document with the requested formats.
4445
4546 ```python
46−# Scrape a website:
47−scrape_result = app.scrape_url('firecrawl.dev', formats=['markdown', 'html'])
47+# Scrape a website (v2):
48+scrape_result = firecrawl.scrape('https://firecrawl.dev', formats=['markdown', 'html'])
4849 print(scrape_result)
4950 ```
5051
5152 ### Crawling a Website
5253
53−To crawl a website, use the `crawl_url` method. It takes the starting URL and optional parameters as arguments. The `params` argument allows you to specify additional options for the crawl job, such as the maximum number of pages to crawl, allowed domains, and the output format.
54+To crawl a website, use the `crawl` method. It takes the starting URL and optional parameters as arguments. You can control depth, limits, formats, and more.
5455
5556 ```python
56−crawl_status = app.crawl_url(
57+crawl_status = firecrawl.crawl(
5758 'https://firecrawl.dev',
5859 limit=100,
5960 scrape_options=ScrapeOptions(formats=['markdown', 'html']),
@@ -66,23 +67,23 @@ print(crawl_status) · 6 unmodified lines
6667
6768 <Tip>Looking for async operations? Check out the [Async Class](#async-class) section below.</Tip>
6869
69−To crawl a website asynchronously, use the `crawl_url_async` method. It returns the crawl `ID` which you can use to check the status of the crawl job. It takes the starting URL and optional parameters as arguments. The `params` argument allows you to specify additional options for the crawl job, such as the maximum number of pages to crawl, allowed domains, and the output format.
70+To enqueue a crawl asynchronously, use `start_crawl`. It returns the crawl `ID` which you can use to check the status of the crawl job.
7071
7172 ```python
72−crawl_status = app.async_crawl_url(
73+crawl_job = firecrawl.start_crawl(
7374 'https://firecrawl.dev',
7475 limit=100,
7576 scrape_options=ScrapeOptions(formats=['markdown', 'html']),
7677 )
77−print(crawl_status)
78+print(crawl_job)
7879 ```
7980
8081 ### Checking Crawl Status
8182
82−To check the status of a crawl job, use the `check_crawl_status` method. It takes the job ID as a parameter and returns the current status of the crawl job.
83+To check the status of a crawl job, use the `get_crawl_status` method. It takes the job ID as a parameter and returns the current status of the crawl job.
8384
8485 ```python
85−crawl_status = app.check_crawl_status("<crawl_id>")
86+crawl_status = firecrawl.get_crawl_status("<crawl_id>")
8687 print(crawl_status)
8788 ```
8889
@@ -91,17 +92,17 @@ print(crawl_status) · 2 unmodified lines
9192 To cancel an asynchronous crawl job, use the `cancel_crawl` method. It takes the job ID of the asynchronous crawl as a parameter and returns the cancellation status.
9293
9394 ```python
94−cancel_crawl = app.cancel_crawl(id)
95+cancel_crawl = firecrawl.cancel_crawl(id)
9596 print(cancel_crawl)
9697 ```
9798
9899 ### Map a Website
99100
100−Use `map_url` to generate a list of URLs from a website. The `params` argument let you customize the mapping process, including options to exclude subdomains or to utilize the sitemap.
101+Use `map` to generate a list of URLs from a website. Options let you customize the mapping process, including whether to use the sitemap or include subdomains.
101102
102103 ```python
103−# Map a website:
104−map_result = app.map_url('https://firecrawl.dev')
104+# Map a website (v2):
105+map_result = firecrawl.map('https://firecrawl.dev')
105106 print(map_result)
106107 ```
107108
@@ -152,20 +153,35 @@ The SDK handles errors returned by the Firecrawl API and raises appropriate exce · 44 unmodified lines
152153
153154 ## Async Class
154155
155−For async operations, you can use the `AsyncFirecrawlApp` class. Its methods are the same as the `FirecrawlApp` class, but they don't block the main thread.
156+For async operations, you can use the `AsyncFirecrawl` class. Its methods mirror the `Firecrawl` class, but you `await` them.
156157
157158 ```python
158−from firecrawl import AsyncFirecrawlApp
159+from firecrawl import AsyncFirecrawl
159160
160−app = AsyncFirecrawlApp(api_key="YOUR_API_KEY")
161+firecrawl = AsyncFirecrawl(api_key="YOUR_API_KEY")
161162
162−# Async Scrape
163+# Async Scrape (v2)
163164 async def example_scrape():
164− scrape_result = await app.scrape_url(url="https://example.com")
165+ scrape_result = await firecrawl.scrape(url="https://example.com")
165166 print(scrape_result)
166167
167−# Async Crawl
168+# Async Crawl (v2)
168169 async def example_crawl():
169− crawl_result = await app.crawl_url(url="https://example.com")
170+ crawl_result = await firecrawl.crawl(url="https://example.com")
170171 print(crawl_result)
172+```
173+
174+## v1 compatibility
175+
176+For legacy code paths, v1 remains available under `firecrawl.v1` with the original method names.
177+
178+```python
179+from firecrawl import Firecrawl
180+
181+firecrawl = Firecrawl(api_key="YOUR_API_KEY")
182+
183+# v1 methods (feature‑frozen)
184+doc_v1 = firecrawl.v1.scrape_url('https://firecrawl.dev', formats=['markdown', 'html'])
185+crawl_v1 = firecrawl.v1.crawl_url('https://firecrawl.dev', limit=100)
186+map_v1 = firecrawl.v1.map_url('https://firecrawl.dev')
171187 ```
apps/python-sdk/firecrawl/__tests__/e2e/v2/.env.example+5−2
@@ -1,2 +1,5 @@
1−API_URL=http://localhost:3002
2−API_KEY=fc-YOUR_API_KEY
1+FIRECRAWL_API_KEY=<your api key>
2+FIRECRAWL_API_URL=https://api.firecrawl.dev
3+
4+# TESTING PURPOSE:
5+IDMUX_URL=
apps/python-sdk/firecrawl/__tests__/e2e/v2/aio/test_aio_map.py+0−2
@@ -17,7 +17,6 @@ if not os.getenv("API_URL"): · 16 unmodified lines
1717 async def test_async_map_minimal():
1818 client = AsyncFirecrawl(api_key=os.getenv("API_KEY"), api_url=os.getenv("API_URL"))
1919 resp = await client.v2.map("https://docs.firecrawl.dev")
20− assert resp.success is True
2120 assert hasattr(resp, "links") and isinstance(resp.links, list)
2221 if resp.links:
2322 first = resp.links[0]
@@ -36,7 +35,6 @@ async def test_async_map_with_all_params(sitemap): · 12 unmodified lines
3635 sitemap=sitemap,
3736 timeout=15000,
3837 )
39− assert resp.success is True
4038 assert hasattr(resp, "links") and isinstance(resp.links, list)
4139 assert len(resp.links) <= 10
4240
apps/python-sdk/firecrawl/__tests__/e2e/v2/conftest.py+73−0
@@ -0,0 +1,73 @@
1+import os
2+import json
3+import pytest
4+import requests
5+from dotenv import load_dotenv
6+
7+load_dotenv()
8+
9+def _idmux(identity_request: dict) -> dict:
10+ idmux_url = os.getenv("IDMUX_URL")
11+ if not idmux_url:
12+ raise EnvironmentError("IDMUX_URL is not set. E2E tests must use idmux for credentials.")
13+ run_number = int(os.getenv("GITHUB_RUN_NUMBER") or 0)
14+ payload = {
15+ "refName": os.getenv("GITHUB_REF_NAME") or "local",
16+ "runNumber": run_number,
17+ "concurrency": identity_request.get("concurrency", 100),
18+ **identity_request,
19+ }
20+ resp = requests.post(idmux_url + "/", json=payload)
21+ resp.raise_for_status()
22+ return resp.json()
23+
24+@pytest.fixture(scope="session")
25+def api_url():
26+ # Prefer TEST_URL, then FIRECRAWL_API_URL (for parity with JS), then legacy API_URL
27+ return (
28+ os.getenv("TEST_URL")
29+ or os.getenv("FIRECRAWL_API_URL")
30+ or os.getenv("API_URL")
31+ or "https://api.firecrawl.dev"
32+ )
33+
34+# Resolve identity and export environment at import time so tests that read env at module import succeed
35+_IDENTITY = None
36+_API_URL = (
37+ os.getenv("TEST_URL")
38+ or os.getenv("FIRECRAWL_API_URL")
39+ or os.getenv("API_URL")
40+ or "https://api.firecrawl.dev"
41+)
42+
43+_IDMUX_URL = os.getenv("IDMUX_URL")
44+if _IDMUX_URL:
45+ run_name = os.getenv("PYTEST_RUN_NAME") or "py-e2e"
46+ # If IDMUX_URL is set, idmux MUST succeed; do not silently fall back
47+ _IDENTITY = _idmux({"name": run_name})
48+ os.environ["API_KEY"] = _IDENTITY.get("apiKey", "")
49+ os.environ["API_URL"] = _API_URL
50+
51+@pytest.fixture(scope="session")
52+def api_identity():
53+ return _IDENTITY or {"apiKey": os.getenv("API_KEY") or "", "teamId": os.getenv("TEST_TEAM_ID") or os.getenv("TEAM_ID") or ""}
54+
55+@pytest.fixture(autouse=True)
56+def _inject_client(request, api_identity, api_url):
57+ # For class-based tests that rely on self.client, inject a client if missing
58+ inst = getattr(request, "instance", None)
59+ if inst is not None and not hasattr(inst, "client"):
60+ try:
61+ from firecrawl import Firecrawl
62+ inst.client = Firecrawl(api_key=api_identity.get("apiKey", ""), api_url=api_url)
63+ except Exception:
64+ pass
65+ # For function-based modules that expect a module-level `firecrawl` symbol
66+ mod = getattr(request, "module", None)
67+ if mod is not None and not hasattr(mod, "firecrawl"):
68+ try:
69+ from firecrawl import Firecrawl
70+ setattr(mod, "firecrawl", Firecrawl(api_key=api_identity.get("apiKey", ""), api_url=api_url))
71+ except Exception:
72+ pass
73+
apps/python-sdk/firecrawl/__tests__/e2e/v2/test_map.py+1−4
@@ -24,7 +24,6 @@ class TestMapE2E: · 23 unmodified lines
2424 def test_map_minimal_request(self):
2525 resp = self.client.map("https://docs.firecrawl.dev")
2626
27− assert hasattr(resp, "success") and resp.success is True
2827 assert hasattr(resp, "links") and resp.links is not None
2928 assert isinstance(resp.links, list)
3029
@@ -46,13 +45,11 @@ class TestMapE2E: · 15 unmodified lines
4645 resp = self.client.map(
4746 "https://docs.firecrawl.dev",
4847 search="docs",
49− include_subdomains=True,
50− limit=10,
48+ include_subdomains=True, limit=10,
5149 sitemap=sitemap,
5250 timeout=15000,
5351 )
5452
55− assert hasattr(resp, "success") and resp.success is True
5653 assert hasattr(resp, "links") and isinstance(resp.links, list)
5754
5855 # Limit should be respected (server-side)
apps/python-sdk/firecrawl/__tests__/e2e/v2/test_search.py+0−6
@@ -5,12 +5,6 @@ from firecrawl.types import SearchData, SearchResult, Document, ScrapeFormats, S · 4 unmodified lines
55
66 load_dotenv()
77
8−if not os.getenv("API_KEY"):
9− raise ValueError("API_KEY is not set")
10−
11−if not os.getenv("API_URL"):
12− raise ValueError("API_URL is not set")
13−
148 firecrawl = Firecrawl(api_key=os.getenv("API_KEY"), api_url=os.getenv("API_URL"))
159
1610 def _collect_texts(entries):
apps/python-sdk/firecrawl/__tests__/e2e/v2/test_usage.py+1−6
@@ -4,15 +4,10 @@ from firecrawl import Firecrawl · 3 unmodified lines
44
55 load_dotenv()
66
7−if not os.getenv("API_KEY"):
8− raise ValueError("API_KEY is not set")
9−
10−if not os.getenv("API_URL"):
11− raise ValueError("API_URL is not set")
12−
137
148 class TestUsageE2E:
159 def setup_method(self):
10+ # Environment is exported by conftest at import time
1611 self.client = Firecrawl(api_key=os.getenv("API_KEY"), api_url=os.getenv("API_URL"))
1712
1813 def test_get_concurrency(self):
apps/python-sdk/firecrawl/__tests__/e2e/v2/test_watcher.py+1−0
@@ -14,6 +14,7 @@ if not os.getenv("API_URL"): · 13 unmodified lines
1414
1515 class TestWatcherE2E:
1616 def setup_method(self):
17+ from firecrawl import Firecrawl
1718 self.client = Firecrawl(api_key=os.getenv("API_KEY"), api_url=os.getenv("API_URL"))
1819
1920 def test_crawl_watcher(self):
apps/python-sdk/firecrawl/client.py+39−17
@@ -113,20 +113,18 @@ class AsyncV2Proxy: · 112 unmodified lines
113113
114114 class Firecrawl:
115115 """
116− Unified Firecrawl client with version-specific access patterns.
117−
118− This class provides a unified interface that allows users to access
119− different API versions through version-specific attributes (v1, v2)
120− while defaulting to v2 for direct method calls.
116+ Unified Firecrawl client (v2 by default, v1 under ``.v1``).
117+
118+ Provides a single entrypoint that exposes the latest API directly while
119+ keeping a feature-frozen v1 available for incremental migration.
121120 """
122121
123122 def __init__(self, api_key: str = None, api_url: str = "https://api.firecrawl.dev"):
124− """
125− Initialize the unified Firecrawl client.
126−
123+ """Initialize the unified client.
124+
127125 Args:
128− api_key: Your Firecrawl API key
129− api_url: The Firecrawl API URL (defaults to production)
126+ api_key: Firecrawl API key (or set ``FIRECRAWL_API_KEY``)
127+ api_url: Base API URL (defaults to production)
130128 """
131129 self.api_key = api_key
132130 self.api_url = api_url
@@ -164,13 +162,7 @@ class Firecrawl: · 31 unmodified lines
164162 self.watcher = self._v2_client.watcher
165163
166164 class AsyncFirecrawl:
167− """
168− Unified Firecrawl client with version-specific access patterns.
169−
170− This class provides a unified interface that allows users to access
171− different API versions through version-specific attributes (v1, v2)
172− while defaulting to v2 for direct method calls.
173− """
165+ """Async unified Firecrawl client (v2 by default, v1 under ``.v1``)."""
174166
175167 def __init__(self, api_key: str = None, api_url: str = "https://api.firecrawl.dev"):
176168 self.api_key = api_key
@@ -185,6 +177,36 @@ class AsyncFirecrawl: · 8 unmodified lines
185177 self.v1 = AsyncV1Proxy(self._v1_client) if self._v1_client else None
186178 self.v2 = AsyncV2Proxy(self._v2_client)
187179
180+ # Expose v2 async surface directly on the top-level client for ergonomic access
181+ # Keep method names aligned with the sync client
182+ self.scrape = self._v2_client.scrape
183+ self.search = self._v2_client.search
184+ self.map = self._v2_client.map
185+
186+ self.start_crawl = self._v2_client.start_crawl
187+ self.get_crawl_status = self._v2_client.get_crawl_status
188+ self.cancel_crawl = self._v2_client.cancel_crawl
189+ self.crawl = self._v2_client.crawl
190+ self.get_crawl_errors = self._v2_client.get_crawl_errors
191+ self.active_crawls = self._v2_client.active_crawls
192+ self.crawl_params_preview = self._v2_client.crawl_params_preview
193+
194+ self.start_batch_scrape = self._v2_client.start_batch_scrape
195+ self.get_batch_scrape_status = self._v2_client.get_batch_scrape_status
196+ self.cancel_batch_scrape = self._v2_client.cancel_batch_scrape
197+ self.batch_scrape = self._v2_client.batch_scrape
198+ self.get_batch_scrape_errors = self._v2_client.get_batch_scrape_errors
199+
200+ self.start_extract = self._v2_client.start_extract
201+ self.get_extract_status = self._v2_client.get_extract_status
202+ self.extract = self._v2_client.extract
203+
204+ self.get_concurrency = self._v2_client.get_concurrency
205+ self.get_credit_usage = self._v2_client.get_credit_usage
206+ self.get_token_usage = self._v2_client.get_token_usage
207+
208+ self.watcher = self._v2_client.watcher
209+
188210 # Export Firecrawl as an alias for FirecrawlApp
189211 FirecrawlApp = Firecrawl
190212 AsyncFirecrawlApp = AsyncFirecrawl
apps/python-sdk/firecrawl/firecrawl.backup.pyrenamed from apps/python-sdk/firecrawl/DELETE_firecrawl.py+0−0

File renamed without changes.

apps/python-sdk/firecrawl/v2/client.py+128−14
@@ -408,8 +408,18 @@ class FirecrawlClient: · 407 unmodified lines
408408 sitemap: Optional[Literal["only", "include", "skip"]] = None,
409409 timeout: Optional[int] = None,
410410 ) -> MapData:
411− """
412− Map a URL and return discovered links (with optional titles/descriptions).
411+ """Map a URL and return discovered links.
412+
413+ Args:
414+ url: Root URL to explore
415+ search: Optional substring filter for discovered links
416+ include_subdomains: Whether to include subdomains
417+ limit: Maximum number of links to return
418+ sitemap: Sitemap usage mode ("only" | "include" | "skip")
419+ timeout: Request timeout in milliseconds
420+
421+ Returns:
422+ MapData containing the discovered links
413423 """
414424 options = MapOptions(
415425 search=search,
@@ -434,8 +444,14 @@ class FirecrawlClient: · 18 unmodified lines
434444 return crawl_module.cancel_crawl(self.http_client, crawl_id)
435445
436446 def crawl_params_preview(self, url: str, prompt: str) -> CrawlParamsData:
437− """
438− Get crawl parameters from LLM based on URL and prompt.
447+ """Derive crawl parameters from natural-language prompt.
448+
449+ Args:
450+ url: Root URL
451+ prompt: Instruction describing how to crawl
452+
453+ Returns:
454+ CrawlParamsData with normalized crawl configuration
439455 """
440456 request = CrawlParamsRequest(url=url, prompt=prompt)
441457 return crawl_module.crawl_params_preview(self.http_client, request)
@@ -453,8 +469,21 @@ class FirecrawlClient: · 11 unmodified lines
453469 scrape_options: Optional['ScrapeOptions'] = None,
454470 ignore_invalid_urls: Optional[bool] = None,
455471 ):
456− """
457− Start an extract job (non-blocking). Use get_extract_status to poll.
472+ """Start an extract job (non-blocking).
473+
474+ Args:
475+ urls: URLs to extract from (optional)
476+ prompt: Natural-language instruction for extraction
477+ schema: Target JSON schema for the output
478+ system_prompt: Optional system instruction
479+ allow_external_links: Allow hyperlinks in output
480+ enable_web_search: Whether to augment with web search
481+ show_sources: Include per-field/source mapping when available
482+ scrape_options: Scrape options applied prior to extraction
483+ ignore_invalid_urls: Skip invalid URLs instead of failing
484+
485+ Returns:
486+ Response payload with job id/status (poll with get_extract_status)
458487 """
459488 return extract_module.start_extract(
460489 self.http_client,
@@ -484,8 +513,23 @@ class FirecrawlClient: · 23 unmodified lines
484513 poll_interval: int = 2,
485514 timeout: Optional[int] = None,
486515 ):
487− """
488− Extract structured data using the v2 client surface, waiting until completion.
516+ """Extract structured data and wait until completion.
517+
518+ Args:
519+ urls: URLs to extract from (optional)
520+ prompt: Natural-language instruction for extraction
521+ schema: Target JSON schema for the output
522+ system_prompt: Optional system instruction
523+ allow_external_links: Allow hyperlinks in output
524+ enable_web_search: Whether to augment with web search
525+ show_sources: Include per-field/source mapping when available
526+ scrape_options: Scrape options applied prior to extraction
527+ ignore_invalid_urls: Skip invalid URLs instead of failing
528+ poll_interval: Seconds between status checks
529+ timeout: Maximum seconds to wait (None for no timeout)
530+
531+ Returns:
532+ Final extract response when completed
489533 """
490534 return extract_module.extract(
491535 self.http_client,
@@ -533,8 +577,39 @@ class FirecrawlClient: · 41 unmodified lines
533577 integration: Optional[str] = None,
534578 idempotency_key: Optional[str] = None,
535579 ):
536− """
537− Start a batch scrape job over multiple URLs.
580+ """Start a batch scrape job over multiple URLs (non-blocking).
581+
582+ Args:
583+ urls: List of URLs to scrape
584+ formats: Output formats to collect per URL
585+ headers: HTTP headers
586+ include_tags: HTML tags to include
587+ exclude_tags: HTML tags to exclude
588+ only_main_content: Restrict scraping to main content
589+ timeout: Per-request timeout in milliseconds
590+ wait_for: Wait condition in milliseconds
591+ mobile: Emulate mobile viewport
592+ parsers: Parser list (e.g., ["pdf"])
593+ actions: Browser actions to perform
594+ location: Location settings
595+ skip_tls_verification: Skip TLS verification
596+ remove_base64_images: Remove base64 images from output
597+ fast_mode: Prefer faster scraping modes
598+ use_mock: Use a mock data source (internal/testing)
599+ block_ads: Block ads during scraping
600+ proxy: Proxy setting
601+ max_age: Cache max age
602+ store_in_cache: Whether to store results in cache
603+ webhook: Webhook configuration
604+ append_to_id: Append to an existing batch job
605+ ignore_invalid_urls: Skip invalid URLs without failing
606+ max_concurrency: Max concurrent scrapes
607+ zero_data_retention: Delete data after 24 hours
608+ integration: Integration tag/name
609+ idempotency_key: Header used to deduplicate starts
610+
611+ Returns:
612+ Response payload with job id (poll with get_batch_scrape_status)
538613 """
539614 options = ScrapeOptions(
540615 **{k: v for k, v in dict(
@@ -574,18 +649,47 @@ class FirecrawlClient: · 33 unmodified lines
574649 )
575650
576651 def get_batch_scrape_status(self, job_id: str):
577− """Get current status and any scraped data for a batch scrape job."""
652+ """Get current status and any scraped data for a batch job.
653+
654+ Args:
655+ job_id: Batch job ID
656+
657+ Returns:
658+ Status payload including counts and partial data
659+ """
578660 return batch_module.get_batch_scrape_status(self.http_client, job_id)
579661
580662 def cancel_batch_scrape(self, job_id: str) -> bool:
581− """Cancel a running batch scrape job."""
663+ """Cancel a running batch scrape job.
664+
665+ Args:
666+ job_id: Batch job ID
667+
668+ Returns:
669+ True if the job was cancelled
670+ """
582671 return batch_module.cancel_batch_scrape(self.http_client, job_id)
583672
584673 def get_batch_scrape_errors(self, job_id: str):
585− """Retrieve error details for a batch scrape job."""
674+ """Retrieve error details for a batch scrape job.
675+
676+ Args:
677+ job_id: Batch job ID
678+
679+ Returns:
680+ Errors and robots-blocked URLs for the job
681+ """
586682 return batch_methods.get_batch_scrape_errors(self.http_client, job_id)
587683
588684 def get_extract_status(self, job_id: str):
685+ """Get the current status (and data if completed) of an extract job.
686+
687+ Args:
688+ job_id: Extract job ID
689+
690+ Returns:
691+ Extract response payload with status and optional data
692+ """
589693 return extract_module.get_extract_status(self.http_client, job_id)
590694
591695 def get_concurrency(self):
@@ -608,7 +712,17 @@ class FirecrawlClient: · 16 unmodified lines
608712 poll_interval: int = 2,
609713 timeout: Optional[int] = None,
610714 ) -> Watcher:
611− """Create a polling-based watcher for crawl or batch jobs."""
715+ """Create a watcher for crawl or batch jobs.
716+
717+ Args:
718+ job_id: Job ID to watch
719+ kind: Job kind ("crawl" or "batch")
720+ poll_interval: Seconds between status checks
721+ timeout: Maximum seconds to watch (None for no timeout)
722+
723+ Returns:
724+ Watcher instance
725+ """
612726 return Watcher(self, job_id, kind=kind, poll_interval=poll_interval, timeout=timeout)
613727
614728 def batch_scrape(
apps/python-sdk/firecrawl/v2/methods/aio/crawl.py+1−2
@@ -160,8 +160,7 @@ async def get_active_crawls(client: AsyncHttpClient) -> ActiveCrawlsResponse: · 159 unmodified lines
160160 body = response.json()
161161 if not body.get("success"):
162162 raise Exception(body.get("error", "Unknown error occurred"))
163− data = body.get("data", {})
164− crawls_in = data.get("crawls", [])
163+ crawls_in = body.get("crawls", [])
165164 normalized = []
166165 for c in crawls_in:
167166 if isinstance(c, dict):
apps/python-sdk/firecrawl/v2/methods/aio/usage.py+2−2
@@ -18,7 +18,7 @@ async def get_concurrency(client: AsyncHttpClient) -> ConcurrencyCheck: · 17 unmodified lines
1818
1919
2020 async def get_credit_usage(client: AsyncHttpClient) -> CreditUsage:
21− resp = await client.get("/v2/team/credit-usage")
21+ resp = await client.get("/v2/credit-usage")
2222 if resp.status_code >= 400:
2323 handle_response_error(resp, "get credit usage")
2424 body = resp.json()
@@ -29,7 +29,7 @@ async def get_credit_usage(client: AsyncHttpClient) -> CreditUsage: · 4 unmodified lines
2929
3030
3131 async def get_token_usage(client: AsyncHttpClient) -> TokenUsage:
32− resp = await client.get("/v2/team/token-usage")
32+ resp = await client.get("/v2/token-usage")
3333 if resp.status_code >= 400:
3434 handle_response_error(resp, "get token usage")
3535 body = resp.json()