Fetch many URLs asynchronously and poll for results.
Start many browser captures, poll one run ID, and consume paginated Fetch results as they finish. Batched Fetch is the async fan-out path for processing a list of URLs that share the same settings, without holding open one request per page.
# Start a run
curl -X POST https://api.expand.ai/v1/fetch/batched \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"urls":["https://example.com","https://example.com/about"]}'
# Poll the run ID it returns
curl "https://api.expand.ai/v1/fetch/batched/019...?limit=10&offset=0" \
URLs
-> POST /v1/fetch/batched
-> { id }
-> GET /v1/fetch/batched/{id}
-> status + paginated results| Use Batched Fetch when | Use single Fetch when |
|---|---|
| You have many URLs with the same settings. | You need one URL now. |
| Your pipeline can poll asynchronously. | You need screenshots, summaries, State JSON, Appendix, or Highlights. |
| You want results paged from one run ID. | You need the full single-request output model. |
Jump to: Start a Run · Poll Results · API Reference
Batched Fetch is API/SDK-only today. There is no CLI command or MCP tool for starting a batch.
Send all the URLs in one POST. The call returns once Hatchet durably accepts the run command; billing reservation, lifecycle projection, and browser captures continue asynchronously in the background.
curl -X POST https://api.expand.ai/v1/fetch/batched \
-H "x-expand-api-key: $EXPAND_API_KEY" \
-H "x-idempotency-key: fastenersolutions:2026-07" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"https://example.com",
"https://example.com/about"
]
}'{
"id": "019..."
}urls is required and must be non-empty. include is optional, and the only public browserConfig field at start is scrollFullPage. Unknown top-level keys are rejected.
Keep the run ID. Every status check and every page of results is read back through it.
An immediate poll can return QUEUED with no results while the accepted command is being projected. This is expected. Billing is checked authoritatively during that initialization, so a run can later become FAILED even though the start request was accepted.
When x-idempotency-key is omitted, the API generates a unique key. Send a stable key when the caller needs to retry a separate request or recover after a process restart. Retrying the same payload with the same explicit key is safe: the API returns a successful response with the same run ID and does not create or charge for another run.
Only reusing the same key with a different request body returns 409 BatchedIdempotencyConflict:
{
"_tag": "BatchedIdempotencyConflict",
"existingRunId": "019...",
"reason": "payload_mismatch"
}Treat payload_mismatch as a client bug rather than polling a run created for different input.
fastenersolutions:2026-07:<digest>.Read status and results back with the run ID. Page through results with limit and offset.
curl "https://api.expand.ai/v1/fetch/batched/019...?limit=10&offset=0" \
-H "x-expand-api-key: $EXPAND_API_KEY"Keep polling while batchedStatus is QUEUED or RUNNING. Stop once it reaches a terminal status.
const terminal = new Set(["COMPLETED", "FAILED", "CANCELLED"])
let status = await client.getBatched(run.id)
while (!terminal.has(status.batchedStatus)) {
await new Promise((resolve) => setTimeout(resolve, 1000))
status = await client.getBatched(run.id)
}FAILED or CANCELLED means stop polling the run.status before using it.batchedStatus describes the whole run. The top-level status describes the current page of results, and each result has its own persisted status.
| Field | Meaning | How to use it |
|---|---|---|
batchedStatus | Overall run status. | Decide whether to keep polling the run. |
status | Current page status, including extraction quality. | Decide whether this page is stable and its requested content was extracted. |
results[].status |
batchedStatus = whole run
status = current pageQUEUED means the run was accepted but may not be executing yet.RUNNING means work is still in progress.COMPLETED on batchedStatus means the run is done, not that every URL produced Markdown.FAILED and CANCELLED are terminal.COMPLETED only when its rows are stable and requested content extraction succeeded. Requested content can legitimately be empty.FAILED after a completed capture if snapshot content could not be read or rendered. Successful siblings on that page remain usable.COMPLETED while the whole run is still RUNNING. Keep the two fields separate in your code; do not collapse them into one status.A poll returns the run status, pagination metadata, and a page of per-URL results.
{
"id": "019...",
"status": "COMPLETED",
"batchedStatus": "RUNNING",
"totalUrls": 20,
"pagination": {
"total": 12,
Handle each result independently. A batch can finish even when one URL was blocked, failed, or redirected. Use results[].status rather than missing Markdown to identify failed items. A blocked result remains SUCCEEDED and carries data.blocked; fully populated siblings remain usable.
If no poll include.markdown is provided, Batched Fetch normalizes results to Markdown by default. For the exact per-result schema, see the API Reference.
Batched results are paginated. Fetch page 1 with offset=0, then keep increasing offset by limit while pagination.hasMore is true.
offset 0 -> 10 results
offset 10 -> 10 results
offset 20 -> ...limit defaults to 10 and accepts 1 through 100.offset defaults to 0 and must be non-negative.pagination.total is the number of available result rows for the run.pagination.hasMore tells you whether to request the next page.curl "https://api.expand.ai/v1/fetch/batched/019...?limit=100&offset=100" \
-H "x-expand-api-key: $EXPAND_API_KEY"status can differ across pages while the run is still active.Batched Fetch is item-oriented. Treat the batch as a container for many Fetch attempts, then inspect each result before using it.
data.blocked, for example blockedType: "botProtection".data.response.originStatusCode helps classify each outcome, for example 403 on a blocked page.data.response.FAILED page can indicate an extraction failure; missing Markdown on a COMPLETED page can be legitimate empty content.limit/offset page with backoff to retry reading existing capture artifacts without another capture or charge.COMPLETED on the run does not override a page-level extraction failure.Retry a start request with the same key and payload after an ambiguous timeout. The API returns the same run ID if the first request already created the run.
| Capability | Batched Fetch support | Notes |
|---|---|---|
| Markdown | Supported | Default result content. |
| HTML | Supported on completed results | Request through include. |
| Meta | Supported | Included by default when available. |
Polling rejects screenshot, json, and appendix include fields with BatchedIncludeUnsupported.
Batched Fetch is narrower than single Fetch by design. Use it when many URLs and asynchronous collection matter more than every single Fetch artifact. For the full set of fields and nested defaults, see Include Options.
The SDKs wrap the same two calls: batched to start a run, getBatched to poll it.
import { ExpandClient } from "@expandai/sdk"
const client = new ExpandClient()
const run = await client.batched({
urls: ["https://example.com", "https://example.com/about"],
})
let page = await client.getBatched(run.id, { limit: "10", offset: "0" })
while (page.batchedStatus === "QUEUED" || page.batchedStatus === "RUNNING") {
batched() generates one idempotencyKey and reuses it for the client's configured retry policy.idempotencyKey to deduplicate separate calls or calls made after a process restart.ExpandClientApiError with status 409 only for a different payload; its body contains existingRunId and reason: "payload_mismatch".getBatched query params are strings today, so the examples use "10" and "0".from expandai import BatchedParams, Expand
client = Expand()
run = client.batched(BatchedParams(
urls=["https://example.com", "https://example.com/about"],
))
status = client.get_batched(run.id)
print(status.results)batched() generates one idempotency_key and reuses it for the client's configured retry policy.idempotency_key to deduplicate separate calls or calls made after a process restart.ExpandAPIError with status code 409 only for a different payload; its body contains existingRunId and reason: "payload_mismatch".CANCELLING while running items drain, or CANCELLED when no work remains; queued items are cancelled immediately.status is COMPLETED while batchedStatus is still RUNNING.COMPLETED means every item has Markdown.screenshot, json, appendix) on polling.payload_mismatch as a recoverable duplicate instead of fixing key reuse.scrollFullPage behavior.| Persisted state for one URL. |
| Handle successful, failed, queued, running, and cancelled URLs independently. |
| Links | Supported on completed results | Request through include.links. |
| Response info | Basic metadata supported | Batched polling returns URL/status metadata, but not response headers today. |
browserConfig.scrollFullPage | Supported at start | Applies to every child fetch. |
| Screenshots | Not supported in batched results | Use single Fetch. |
State JSON / json | Not supported in batched results | Use single Fetch or JSON Mode. |
| Appendix | Not supported in batched results | Use single Fetch. |
| Highlights/search | Not supported | Use Highlights on a single Fetch or existing snapshot. |