Scrape a URL
Fetches and processes a URL, returning content in one or more formats wrapped in a ScrapeEnvelope.
The response shape is decided by your request, not by how we route it. In precedence order:
async: truein the body, or?async=truein the query string (either one is enough;falsein one never cancelstruein the other) → 202 with aScrapeQueuedEnvelopereceipt, returned immediately. PollGET /v1/scrape/{id}for the result.- Otherwise, a request that needs a full page render — today that is
delay_ms > 0— is held open until the render reaches a terminal state, then returns 200 with the content envelope (successorfailed). If the render is still running when the hold window expires (28 seconds by default), the request returns the 202 receipt instead and the job continues in the background. - Otherwise the request is served by the fast HTTP path and returns 200 with the content envelope, typically in under a second.
A failed fetch is a result, not a transport error. An unreachable host, a refused connection, or a target whose bot protection defeats every proxy tier all come back on the same 200 as a failed envelope — never a 5xx. Narrow on the envelope's status before reading the body. A failure the fast path decides inline is recorded as a failed job, so its envelope normally carries that job's id and GET /v1/scrape/{id} returns the same envelope; if recording the failed job itself fails, the envelope still arrives on the same 200 with id set to null and there is nothing to poll. A failure a held render reaches carries that render's real id. Either way the request transfers no billable bytes and is charged nothing.
A target that rate-limits us is not ByteKit rate-limiting you. The failed envelope's error.code is target_rate_limited when the target rate-limits the request. On the fast path that means our proxy attempts end on an HTTP 429 and no bot protection is identified on any attempt, even if the browser fallback then fails or reaches only an unidentified block page; the inline failure carries error.retry_after_seconds when the target sent a usable Retry-After. On a held render the browser's own answer decides: it observes a 429 on the page itself, or it fails outright after the proxy attempts ended on an HTTP 429. A bot-protection verdict on an earlier fetch attempt does not keep a held render blocked when the browser then observes the 429. A held render never carries error.retry_after_seconds. Retrying a target_rate_limited failure later is likely to work. A held render whose browser reaches an unidentified 403 or 503 page after a proxy 429 is blocked. A 429 that bot protection sends as a block stays blocked when it is the answer that decides the code: the fast path's proxy attempts, or the browser render on a held render. Your own per-key limit is a separate case: it answers HTTP 429 rate_limited with a Retry-After header (see the 429 response).
So a 202 means one of exactly two things: you asked for one with async: true or ?async=true, or the hold window expired. wait_for_selector and wait_until are ScreenshotRequest options, not ScrapeRequest options — sending either to this endpoint returns 422 validation_error, so neither can produce a 202 here.
For AI agents: https://bytekit.com/llms.txt — every docs page is available as markdown by appending .md
Authorization
AuthorizationRequiredBearer <token>API key. Manage keys from the dashboard at app.bytekit.com.
In: header
Request Body
application/jsonRequiredurlRequiredstring"uri"formatsarray<string | object>What to return. Each entry is either a string format (raw, markdown, links
or images) or a json object entry that extracts structured data from the page.
The string "json" is not a format value; the json entry is an object, described
by ScrapeRequestJsonFormat. A list may hold at most one json entry, and a list
holding only the json entry still captures raw. The json entry is accepted on
POST /v1/scrape, POST /v1/scrape/bulk and POST /v1/bulk (scrape entries);
monitor formats accept the string formats alone. Its result is formats.json on the
response, and its charge is described on ScrapeRequestJsonFormat.
["raw"]countrystring"US"Pattern: "^[A-Z]{2}$"delay_msintegerMilliseconds to wait after page load before capturing, for content that renders late. Any value above 0 puts the request on the full browser render path. Unless async: true is also set, the request is then HELD until the render finishes and returns 200 with the content envelope, or returns the 202 receipt if the hold window expires first. 0 (the default) keeps the request on the fast HTTP path, which returns 200 in one round trip.
0Minimum: 0Maximum: 10000timeout_msinteger60000Minimum: 1000Maximum: 90000asyncbooleanWhen true, the request returns 202 with a ScrapeQueuedEnvelope receipt immediately and the scrape runs in the background; poll GET /v1/scrape/{id} for the result. The ?async=true query parameter is a second spelling of this field: the request is async when either one says true, and false in one never cancels true in the other. Asking for a receipt this way outranks every routing consequence: when neither is set (the default) a browser-path request is held to completion instead of being receipted. A 202 can still arrive without either, but only when the hold window expires.
falsewebhook_urlstringWebhook endpoint URL (HTTPS only; http:// rejected with 400 invalid_url, reason scheme_unsupported)
"^https://"Format: "uri"eventsarray<string>Webhook event types to deliver when webhook_url is set. Defaults to all three (queued, completed, failed); send an explicit subset to receive fewer.
["queued","completed","failed"]markdown_modestringMarkdown processing mode. article=article extraction (default), raw=minimal cleanup, llm=compact LLM-optimised output.
"article" | "raw" | "llm"markdown_querystringBM25 query string for relevance-ranked content filtering. Omit or leave empty to disable.
200markdown_linksstringLink rendering style in the markdown output. 'inline' (default) keeps [text](url) as-is. 'text' strips URLs, keeping only anchor text. 'none' strips URLs and removes reference definitions. 'references' converts to numbered [text][N] refs with a Links footer.
"inline" | "references" | "none" | "text"markdown_imagesstringImage retention mode in the markdown output. 'inline' (default) keeps  as-is. 'text' strips the URL, keeping only alt text. 'none' removes images entirely. 'references' converts to numbered ![alt][N] refs with an Images footer. Distinct from markdown_filter_images (which drops low-signal images before conversion).
"inline" | "references" | "none" | "text"with_links_summarybooleanWhen true, appends a 'Links:\n[1]: url\n...' footer to the markdown output, built from the extracted link list. Automatically enabled when markdown_links is 'references'.
with_images_summarybooleanWhen true, appends an 'Images:\n[1]: url\n...' footer to the markdown output, built from the extracted image list.
markdown_compactbooleanCollapse excessive whitespace for a more compact output.
markdown_filter_imagesbooleanFilter low-signal images from the markdown output.
markdown_include_mediabooleanWhen true, formats.links and formats.images return ScrapeScoredLink[] / ScrapeScoredImage[] (rich objects) instead of string[], and a top-level tables array is included. Only effective when markdown is in formats.
markdown_include_warningsbooleanWhen true, enables markdown-pipeline and tag-filter warnings for requests that include the markdown format. Artifact availability warnings such as artifact_unavailable may appear independently of this flag.
markdown_include_statsbooleanWhen true, the response includes a top-level stats object with ScrapeStats (chars, tokens, blocks). Only effective when markdown is in formats.
token_budgetintegerMaximum token count for the markdown output. When set and markdown is in formats, the output is truncated at the last paragraph → sentence → token boundary that keeps the estimated count under this limit. Absent → no truncation. Triggers a markdown_tokens field in the response.
0token_encodingstringTokenizer encoding hint for token_budget. cl100k_base (default) matches GPT-4/4o; o200k_base matches GPT-4o-mini / o-series. Both use the same 4-char/token heuristic internally.
"cl100k_base"Value in: "cl100k_base" | "o200k_base"mobilebooleanWhen true, emulate a mobile device — the request uses a mobile (Android) user agent and a mobile viewport, so the upstream serves the mobile version of the page. Default false (desktop). The value is part of the cache key, so mobile and desktop responses are cached independently.
falsesolve_challengebooleanAttempts to clear a Cloudflare challenge; success is not guaranteed. Billed as a normal browser render.
falseremove_base64_imagesbooleanWhen true (default), base64-encoded data: image sources are stripped from the HTML before markdown conversion. Set to false to preserve base64 images in the pipeline output. Does not affect raw, which always returns the original source unchanged.
trueinclude_tagsarray<string>HTML element names (not CSS selectors) to keep. When non-empty, only content within these elements is included in the markdown output. Applied before exclude_tags. Does not affect raw, which always returns the full page.
[]exclude_tagsarray<string>HTML element names (not CSS selectors) to strip from the markdown output. Applied after include_tags when both are present. Does not affect raw, which always returns the full page.
[]cache_ttlstring | integerHow long a freshly fetched URL may be served from cache. 0 (string or integer) skips the cache read; cache-eligible results are still written. Nh/Nd set a TTL (capped at 168 h / 7 d). Safety-classified WAF husks are returned live but never stored in the shared cache. Honoured on both the synchronous and asynchronous paths; the async success envelope reports the lookup outcome via the cache field.
"48h"customobjectUser-supplied JSON payload, echoed back on the success envelope (sync, async, webhook). Capped at 4096 UTF-8 bytes (Buffer.byteLength). Distinct from the system-owned metadata column. Does NOT affect cache-key inputs — two requests differing only in custom resolve to the same cache entry.
{}clean_markdownbooleanWhen true and markdown is in formats, the converted markdown is sent to Groq LLM for cleaning (boilerplate removal, ad stripping, formatting improvement) before being returned. Graceful degradation: any Groq failure (key unset, 4xx/5xx, timeout, empty response) returns the raw markdown unchanged plus a warning in the warnings array. Billing: 3× credits and bytes applied only when cleaning succeeds.
falseQuery Parameters
asyncbooleanQuery-string spelling of the body async field. true returns the 202 receipt immediately; any other value is ignored. Either spelling turns async on, and false in one never cancels true in the other.
falseScrape reached a terminal state and the body is that terminal envelope: success carrying content, or failed carrying the error envelope. Returned by the fast HTTP path in one round trip, and by a browser-path request without async: true or ?async=true once its render reaches a terminal state inside the hold window. A held render that ends in failure answers 200 with the same failed envelope GET /v1/scrape/{id} returns — narrow on status before reading the body. A definitive upstream outcome is a result, not a transport error: an unreachable host, a refused connection, or a target whose bot protection defeats every proxy tier all answer 200 with the failed envelope, never a 5xx. An inline failure is recorded as a failed job, so its envelope normally carries that job's id and polling it returns the same envelope; id is null only when recording the failed job itself failed. error.http_status is present only when a real upstream HTTP status is known.
The fast path exhausted every proxy tier without a usable response (DNS failure, refused connection, or terminal bot-protection block, which reports blocked). The failure is recorded as a failed job, so id names it and GET /v1/scrape/{id} returns this same envelope. error.http_status is omitted because no upstream HTTP status was received. The request transfers no billable bytes and is charged nothing.