batch_scrape
Fetch many URLs in one call with a shared worker pool. The request is synchronous — it returns the finished results, not a job handle — and every URL comes back with its own status, so one dead page never sinks the batch. Results are also stored for 24 hours and can be re-read with get_batch_results.
Use Cases
Competitor Price Sweeps
Pull the same CSS field off fifty product pages in one request, instead of fifty round trips.
Link Health Checks
Submit a URL list and read http_status per entry. Anything unreachable comes back as failed with the reason attached.
Content Inventories
Feed a sitemap into the batch and collect every title and canonical URL for an audit spreadsheet.
Post-Deploy Verification
Check that a set of critical pages still returns 200 and still contains the element you expect.
Dataset Assembly
Collect the first 5,000 characters of body text from a URL list as the input to a downstream pipeline.
Deferred Collection
Run the batch now and pull the stored results later from a different process with get_batch_results.
Endpoint
/api/v1/tools/batch_scrapeParameters
urls, batch_config.concurrency and extraction_template.fields change what it does. Everything else is validated and then ignored; each one says so in the table below.| Name | Type | Required | Default | Description |
|---|---|---|---|---|
urls | array | Required | - | The URLs to fetch. Between 1 and 50 entries; a longer list is rejected rather than truncated. Example: [{ "url": "https://example.com/widget-a", "id": "a" }] |
batch_config | object | Optional | - | Execution settings for the batch. Example: { "concurrency": 5 } |
extraction_template | object | Optional | - | Fields to pull out of every page in the batch. Example: { "fields": [{ "name": "price", "selector": ".price" }] } |
output_config | object | Optional | - | Accepted and ignored in full. Responses are always JSON in the shape shown below; `format`, `include_metadata`, `include_errors` and `flatten_results` have no effect. Example: { "format": "json" } |
options | object | Optional | - | Accepted and ignored, except that `javascript_enabled: true` adds an explanatory line to `notes`. `user_agent`, `follow_redirects`, `respect_robots_txt` and `rate_limit_per_domain` do not change fetching on the hosted REST API. Example: { "javascript_enabled": false } |
respect_robots | boolean | Optional | true | Respect each target site's robots.txt. Left at `true`, a URL disallowed for `CrawlForge` is skipped and the rest of the batch runs on — the request still returns 200 with a partial result set rather than a 403, and a skipped URL is not charged. Set it to `false` only for targets you have your own agreement with — the response then carries a `warnings` entry and the override is recorded against your API key. Example: true |
Execution Model
Worth knowing before you size a batch — this endpoint runs inside a 30-second serverless function.
batch_id is a retrieval key, not a job handle.skipped status and are not charged. Individual fetches time out at 8s.failed. The call itself still returns 200.Request Examples
curl -X POST https://crawlforge.dev/api/v1/tools/batch_scrape \
-H "X-API-Key: cf_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": [
{ "url": "https://example.com/widget-a", "id": "a" },
{ "url": "https://example.com/widget-b", "id": "b" }
],
"batch_config": { "concurrency": 5 },
"extraction_template": {
"fields": [
{ "name": "price", "selector": ".price" },
{ "name": "canonical", "selector": "link[rel=canonical]", "attribute": "href" }
]
}
}'Response Example
{ "success": true, "data": { "batch_id": "fd599a27-9ca9-4022-9a57-b02af57c387b", "total": 3, "succeeded": 2, "failed": 1, "skipped": 0, "results": [ { "id": "a", "url": "https://example.com/widget-a", "status": "success", "http_status": 200, "title": "Widget A — Acme", "text": "Widget A$24.00In stock and ready to ship.", "fields": { "price": "$24.00", "canonical": "https://example.com/widget-a", "sku": null }, "field_notes": [ "sku: xpath selectors are not supported on the hosted REST API — use a CSS selector" ] }, { "id": "b", "url": "https://example.com/widget-b", "status": "success", "http_status": 200, "title": "Widget B — Acme", "text": "Widget B$31.50Backordered until March.", "fields": { "price": "$31.50", "canonical": "https://example.com/widget-b", "sku": null } }, { "id": "c", "url": "https://example.com/gone", "status": "failed", "http_status": 404, "error": "HTTP 404" } ], "notes": [ "Results are stored for 24h and retrievable with get_batch_results (batch_id: fd599a27-9ca9-4022-9a57-b02af57c387b)." ], "completed_at": "2026-08-27T02:09:44.001Z" }, "credits_used": 15, "credits_remaining": 990, "processing_time": 3140}data.batch_idPass this to [get_batch_results](/docs/api-reference/tools/get-batch-results) within 24 hours to re-read the full setdata.totalEntries submitted — always equal to the length of `results`data.skippedURLs the time budget never reached. Not chargeddata.results[].idYour `id` if you supplied one, otherwise the array index as a stringdata.results[].status`success`, `failed` or `skipped` — check this per entry, not the HTTP statusdata.results[].textVisible body text with scripts and styles removed, cut at 5,000 charactersdata.results[].fieldsOne key per template field. `null` means the selector matched nothingdata.results[].field_notesPresent only when a field could not be applied — an xpath selector, or CSS the parser rejecteddata.notesBatch-level remarks, including anything you asked for that the hosted API does not docredits_used5 per URL attempted — 3 URLs here, none skipped, so 15Error Handling
Invalid Batch (400 Bad Request)
VALIDATION_ERROR. Raised by an empty urls array, more than 50 entries, a malformed URL, or a concurrency outside 1-10. Nothing is fetched and nothing is charged.
Storage Unavailable (503 Service Unavailable)
STORAGE_UNAVAILABLE. Results are persisted before billing, so if that store cannot be reached the batch is refused rather than run and lost. Nothing is charged. Retry.
Batch Failed (500 Internal Server Error)
TOOL_ERROR. An unexpected fault. Per-URL problems never surface here — they appear as failed entries in a 200 response.
Disallowed by robots.txt (no error — the URL is skipped)
A URL that robots.txt disallows for CrawlForge is left out of the batch rather than failing it: the call still returns 200 with a partial result set and the skipped URL is not charged, so read the per-URL results before treating the batch as complete. Set respect_robots: false to override for targets you have your own agreement with — the override is recorded against your API key. The override does not reach a host on CrawlForge's permanent opt-out list: that one is refused whatever respect_robots is set to, and it is skipped and left uncharged in the same way, with the host named in that result's error. data.notes summarises how many URLs were skipped and why.
succeeded, failed and skipped — or each entry's status — before treating the batch as complete.Credit Cost
skipped because the time budget ran out are not charged, and a batch rejected at validation costs nothing. Failed URLs are charged — the fetch was attempted.What's Included:
Up to 50 URLs per call, fetched concurrently
Title and body text per page (5,000 characters)
CSS field extraction with per-field notes
Per-URL status and HTTP code
24-hour result storage for get_batch_results
Plan Recommendations:
Free Plan: 1,000 one-time trial credits = 200 URLs
Hobby Plan: 5,000 credits = 1,000 URLs ($19/mo)
Professional Plan: 50,000 credits = 10,000 URLs ($99/mo)