# Get scrape bulk job status

Returns the job envelope plus its rehydrated per-URL scrape envelopes.

**Downloading item content.** A `raw` or `markdown` artifact within the
10-MiB inline cap is returned inline. An oversized one is replaced by a
**pre-signed** URL, minted fresh on every read of this endpoint and valid
for **86400 seconds (24 hours)**. The underlying objects are private, so an
unsigned URL returns `401`: download the content inside that window, or
re-poll this endpoint for a fresh signature. An artifact that was never
stored, or whose object cannot be signed, is omitted from `formats` and
reported as `artifact_unavailable` in `warnings`. The private unsigned
storage URL is never returned.

**Cancelled by the platform.** When the account can no longer spend (its bandwidth
balance is exhausted, or the account is deleted) while items are still pending, ByteKit
cancels the job itself. It then reads `status: cancelled` even though you never called
`DELETE /v1/bulk/{id}`, and the cancellation sends no webhook.

**Job ids from other create routes.** The job is looked up by account only, so the id
of a job created by `POST /v1/fetch/bulk` or `POST /v1/bulk` is found here too, not a `404`.
The items listed are the job's scrape rows: a fetch-bulk job's items appear, and a
`POST /v1/bulk` job shows its scrape items but not its screenshots. An id from another
account answers `404`.

**GET** `/v1/scrape/bulk/{id}`

## Parameters

- `id` (path, string, required)
- `cursor` (query, string) — Opaque cursor returned by the preceding page. It is bound to this bulk id, this recovery route, and the exact status filter; cross-scope cursors return 422.
- `limit` (query, integer) — Maximum result rows considered for this page; the 16-MiB response bound may stop sooner.
- `status` (query, string) — Optional item-status filter. A cursor may be reused only with the same filter.

## Responses

### 200 — Bulk job status with rehydrated item envelopes. Each completed scrape
result entry echoes its `custom` payload (resolved per entry as
`entry.custom ?? body.custom`) at the same position it occupies in the
`/v1/scrape` success envelope. The key is omitted when no `custom` was
supplied for that entry.
- `id`
- `status`
- `total`
- `completed`
- `failed`
- `created_at`
- `completed_at`
- `total_credits_charged`
- `total_billed_bytes`
- `estimated_credits`
- `webhook_url`
- `next_cursor`
- `has_more`
- `items`
- `items.schema_version`
- `items.status`
- `items.id`
- `items.url`
- `items.final_url`
- `items.content_type`
- `items.content_length`
- `items.status_code`
- `items.retrieved_at`
- `items.formats`
- `items.formats.raw`
- `items.formats.markdown`
- `items.formats.links`
- `items.formats.links.url`
- `items.formats.links.text`
- `items.formats.links.isInternal`
- `items.formats.links.rel`
- `items.formats.images`
- `items.formats.images.url`
- `items.formats.images.alt`
- `items.formats.images.width`
- `items.formats.images.height`
- `items.formats.images.format`
- `items.formats.images.score`
- `items.formats.json`
- `items.formats.json.data`
- `items.formats.json.validated`
- `items.formats.json.model`
- `items.metadata`
- `items.metadata.title`
- `items.metadata.description`
- `items.metadata.language`
- `items.metadata.canonicalUrl`
- `items.metadata.ogTitle`
- `items.metadata.ogDescription`
- `items.metadata.ogImage`
- `items.metadata.ogUrl`
- `items.metadata.ogSiteName`
- `items.metadata.robots`
- `items.metadata.byline`
- `items.metadata.publishedAt`
- `items.tables`
- `items.tables.kind`
- `items.tables.gfm`
- `items.tables.rows`
- `items.tables.cols`
- `items.warnings`
- `items.warnings.code`
- `items.warnings.message`
- `items.warnings.element`
- `items.stats`
- `items.stats.chars`
- `items.stats.tokens`
- `items.stats.blocks`
- `items.markdown_tokens`
- `items.markdown_tokens.kept`
- `items.markdown_tokens.total`
- `items.markdown_tokens.truncated`
- `items.billing`
- `items.billing.raw_bytes`
- `items.billing.factors`
- `items.billing.factors.name`
- `items.billing.factors.value`
- `items.billing.factors.reason`
- `items.billing.multiplier`
- `items.billing.billed_bytes`
- `items.custom`
- `items.cache`
- `items.cache_age_s`
- `items.error`
- `items.error.code`
- `items.error.message`
- `items.error.http_status`
- `items.error.retry_after_seconds`
- `items.error.failed_at`
- `items.status_url`
- `items.queued_at`
- `items.events`
- `items.type`
- `items.terminal_outcome`

### 202 — Bulk job still processing. `routes/scrape-bulk.ts` ends the poll handler
with `status: isTerminal ? 200 : 202` over an identical body, so this is
the SAME envelope the 200 documents — only the status code and the
reachable `status` values differ. Keep polling until you get a 200
(issue #3533).
- `id`
- `status`
- `total`
- `completed`
- `failed`
- `created_at`
- `completed_at`
- `total_credits_charged`
- `total_billed_bytes`
- `estimated_credits`
- `webhook_url`
- `next_cursor`
- `has_more`
- `items`
- `items.schema_version`
- `items.status`
- `items.id`
- `items.url`
- `items.final_url`
- `items.content_type`
- `items.content_length`
- `items.status_code`
- `items.retrieved_at`
- `items.formats`
- `items.formats.raw`
- `items.formats.markdown`
- `items.formats.links`
- `items.formats.links.url`
- `items.formats.links.text`
- `items.formats.links.isInternal`
- `items.formats.links.rel`
- `items.formats.images`
- `items.formats.images.url`
- `items.formats.images.alt`
- `items.formats.images.width`
- `items.formats.images.height`
- `items.formats.images.format`
- `items.formats.images.score`
- `items.formats.json`
- `items.formats.json.data`
- `items.formats.json.validated`
- `items.formats.json.model`
- `items.metadata`
- `items.metadata.title`
- `items.metadata.description`
- `items.metadata.language`
- `items.metadata.canonicalUrl`
- `items.metadata.ogTitle`
- `items.metadata.ogDescription`
- `items.metadata.ogImage`
- `items.metadata.ogUrl`
- `items.metadata.ogSiteName`
- `items.metadata.robots`
- `items.metadata.byline`
- `items.metadata.publishedAt`
- `items.tables`
- `items.tables.kind`
- `items.tables.gfm`
- `items.tables.rows`
- `items.tables.cols`
- `items.warnings`
- `items.warnings.code`
- `items.warnings.message`
- `items.warnings.element`
- `items.stats`
- `items.stats.chars`
- `items.stats.tokens`
- `items.stats.blocks`
- `items.markdown_tokens`
- `items.markdown_tokens.kept`
- `items.markdown_tokens.total`
- `items.markdown_tokens.truncated`
- `items.billing`
- `items.billing.raw_bytes`
- `items.billing.factors`
- `items.billing.factors.name`
- `items.billing.factors.value`
- `items.billing.factors.reason`
- `items.billing.multiplier`
- `items.billing.billed_bytes`
- `items.custom`
- `items.cache`
- `items.cache_age_s`
- `items.error`
- `items.error.code`
- `items.error.message`
- `items.error.http_status`
- `items.error.retry_after_seconds`
- `items.error.failed_at`
- `items.status_url`
- `items.queued_at`
- `items.events`
- `items.type`
- `items.terminal_outcome`

### 401 — Unauthorized. The request carried no valid credential, and no work was
performed. The unified `Error` envelope carries one of the codes below
in `error.code`; match on the code, never on the prose.

- `invalid_api_key` — a bearer API key was presented and rejected:
  unknown, revoked, expired or malformed. The bearer-only operations
  (`/v1/webhook-deliveries*` among them) also return it when no
  `Authorization` header is sent. `GET /v1/account` is the exception
  to "malformed": it returns `invalid_api_key` only for a key that
  starts `sk_test_` or `sk_live_` and is unknown, revoked or expired.
- `unauthorized` — the `Authorization` header is absent or malformed on
  an operation that also accepts a dashboard session (the dual-auth and
  billing operations). `GET /v1/account` also returns it, for no
  `Authorization` header and for any bearer value that does not start
  `sk_test_` or `sk_live_` (that value is checked as a dashboard
  session token).

Not retryable as-is: supply a valid API key first.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`

### 404 — Resource not found.
- `status`
- `error`
- `error.code`
- `error.message`
- `error.http_status`
- `error.failed_at`
- `error.details`