Scraping
Scrape any URL as HTML, markdown, or structured content — formats, options, latency, and async polling.
For AI agents: https://bytekit.com/llms.txt — every docs page is available as markdown by appending .md
POST /v1/scrape fetches a URL and returns its content in the format you ask for. The same
request shape works for every site; the options you pass determine the output and how long the
call takes.
Rendering & latency
Most scrapes return in well under a second. Some options require ByteKit to fully render the page before it can return content, which takes longer — typically a few seconds. Pass these only when you need them:
| Option | When it adds latency |
|---|---|
delay_ms | Greater than 0 |
async | true (the request is queued and processed in the background) |
The country option routes the request through a geo-located proxy. On its own it does not add
rendering latency; it only does so when combined with one of the options above.
Looking for wait_for_selector or wait_until? Those are not ScrapeRequest options — they
belong to the Screenshot endpoint. Sending them to
/v1/scrape returns 422.
cookies and headers are no longer request options on any capture endpoint. Sending either
returns 400 validation_error.
# A simple, low-latency scrape
curl -X POST https://api.bytekit.com/v1/scrape \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'# Slower: delay_ms forces a full render and waits before returning
curl -X POST https://api.bytekit.com/v1/scrape \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"delay_ms": 2000
}'Options
Every POST /v1/scrape option is listed here by the job it does. Only url is required. The
API reference carries the full request schema.
Output shaping
url— the page to scrape.formats— what to return: any ofraw(the default),markdown,linksandimages. Theformatsobject in the response is keyed by what you asked for.markdown_mode— how the page becomes markdown:article(the default) extracts the article content,rawapplies minimal cleanup, andllmproduces compact, LLM-optimised output.markdown_query— a BM25 query string (up to 200 characters) that filters the markdown by relevance to it. Omit it or leave it empty to disable.include_tagsandexclude_tags— HTML element names (not CSS selectors).include_tagskeeps only the content inside those elements in themarkdownoutput;exclude_tagsstrips them from it.include_tagsis applied first, thenexclude_tags. Neither affectsraw, which always returns the full page.clean_markdown— whentrueandmarkdownis informats, the converted markdown is sent to an LLM (Groq) to remove boilerplate and ads and improve formatting before it is returned. If that step fails for any reason (key unset, 4xx/5xx, timeout, empty response), you get the raw markdown unchanged plus a warning in thewarningsarray. Billing is 3× credits and bytes, applied only when cleaning succeeds.markdown_links— how links are written in the markdown:inline(the default) keeps[text](url),textkeeps only the anchor text,nonestrips URLs and removes reference definitions, andreferencesconverts to numbered[text][N]references with a Links footer.markdown_images— how images are written in the markdown:inline(the default) keeps,textkeeps only the alt text,noneremoves images entirely, andreferencesconverts to numbered![alt][N]references with an Images footer. This is distinct frommarkdown_filter_images, which drops low-signal images before conversion.with_links_summaryandwith_images_summary— append aLinks:orImages:footer, a numbered list of the extracted URLs, to the markdown.with_links_summaryis enabled automatically whenmarkdown_linksisreferences.markdown_compact— collapse excessive whitespace for a more compact output.markdown_filter_images— filter low-signal images out of the markdown output.markdown_include_media— returnformats.linksandformats.imagesas rich scored objects instead of string arrays, and include a top-leveltablesarray. Only effective whenmarkdownis informats.markdown_include_warnings— enable markdown-pipeline and tag-filter warnings for requests that include themarkdownformat. Artifact availability warnings such asartifact_unavailablemay appear independently of this flag.remove_base64_images— base64-encodeddata:image sources are stripped from the HTML before markdown conversion (the default,true). Set it tofalseto keep them. It does not affectraw, which always returns the original source unchanged.
Token control
token_budget— the maximum token count for the markdown output. When set andmarkdownis informats, the output is truncated at the last paragraph, then sentence, then token boundary that keeps the estimated count under the limit. Leave it out for no truncation. Setting it adds amarkdown_tokensfield to the response.token_encoding— the tokenizer hint fortoken_budget:cl100k_base(the default) matches GPT-4/4o ando200k_basematches GPT-4o-mini and the o-series. Both use the same 4-characters- per-token heuristic internally.markdown_include_stats— include a top-levelstatsobject (characters, tokens, blocks) in the response. Only effective whenmarkdownis informats.
Rendering and access
delay_ms— milliseconds to wait after page load before capturing, for content that renders late. See Rendering & latency above.country— a two-letter uppercase country code (defaultUS) that routes the request through a geo-located proxy.mobile— emulate a mobile device: the request uses a mobile (Android) user agent and viewport so the upstream serves its mobile page. The default isfalse(desktop). The value is part of the cache key, so mobile and desktop responses are cached independently.solve_challenge— attempts to clear a Cloudflare challenge. Success is not guaranteed, and it is billed as a normal browser render.timeout_ms— the request timeout in milliseconds, from1000to90000(default60000).
Caching and delivery
cache_ttl— how long a freshly fetched URL may be served from cache.0(string or integer) skips the cache read, though cache-eligible results are still written.NhorNdsets a TTL, capped at 168 h / 7 d. The default is48h. Safety-classified WAF husks are returned live but never stored in the shared cache. It is honoured on both the synchronous and asynchronous paths.async— queue the scrape and get a202receipt immediately; see Async scrapes below.webhook_url— an HTTPS endpoint to notify. Anhttp://URL is rejected with400 invalid_url(reasonscheme_unsupported).events— which webhook events to deliver whenwebhook_urlis set: any ofqueued,completedandfailed. All three are delivered by default; send a subset to receive fewer.custom— a JSON object of your own, echoed back on the success envelope (sync, async and webhook). It is capped at 4096 UTF-8 bytes and does not affect the cache key, so two requests that differ only incustomresolve to the same cache entry.
Examples
A markdown scrape trimmed to a token budget, with the response's token statistics:
curl -X POST https://api.bytekit.com/v1/scrape \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"formats": ["markdown"],
"markdown_mode": "llm",
"token_budget": 4000,
"token_encoding": "o200k_base",
"markdown_include_stats": true
}'A content-only scrape: keep the article and main elements, then strip navigation and footers
from what is left.
curl -X POST https://api.bytekit.com/v1/scrape \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"formats": ["markdown"],
"include_tags": ["article", "main"],
"exclude_tags": ["nav", "footer", "aside"]
}'Response fields
The default format is raw — pass formats to request others (markdown, links,
images). The formats object in the response is keyed by the format you requested.
The scrape_url tool in the MCP server differs: it returns markdown when formats is
omitted. A request that includes markdown bills the 1.5× scrape_md endpoint factor; one that
does not bills 1× (scrape). The API default stays raw so that omitting formats does not
silently raise a request's cost.
Envelope fields are snake_case; the keys inside metadata are the page's own metadata and
stay camelCase.
| Field | Description |
|---|---|
schema_version | Response envelope version (currently 1) |
formats.raw | Page content in the requested format (raw is the default when formats is omitted) |
final_url | URL the fetch landed on after redirects |
content_type | Content-Type reported by the origin |
content_length | Compressed wire bytes — the basis for bandwidth billing, before the endpoint factor (see billing). A page served by a hosted browser reports the browser's full measured transfer |
status_code | HTTP status code of the origin page |
retrieved_at | ISO-8601 UTC timestamp of the fetch |
cache | "hit" or "miss" |
cache_age_s | Age of the cached entry in seconds — present only on a hit |
billing | What the request cost: raw_bytes, the factors applied, the resulting multiplier, and billed_bytes |
metadata.title | Page <title> element |
The X-Scrape-* response headers describe how each request was handled (cache status, timing,
and the final URL) — useful for debugging.
Async scrapes
For long-running pages, pass "async": true to get a 202 Accepted response with a scrape
id prefixed sc_. Poll the result with GET /v1/scrape/{id}.
# Start async scrape
curl -X POST https://api.bytekit.com/v1/scrape \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "async": true}'
# Poll for result
curl https://api.bytekit.com/v1/scrape/sc_... \
-H "Authorization: Bearer sk_live_YOUR_KEY_HERE"Webhook event header
Every outbound webhook POST includes an X-ByteKit-Event header with the event type:
| Event type | Trigger |
|---|---|
bulk.completed | A bulk job finishes (all items processed) |
scrape.completed | An async scrape succeeds, or a scrape monitor delivers a capture (has_change tells a change from a no-change capture) |
scrape.failed | An async scrape fails terminally |
monitor.change_detected | A screenshot monitor fires and detects a change |
monitor.captured | A screenshot monitor fires with no change detected (notify_on: every) |
monitor.suspended | A monitor is suspended because the account ran out of balance (reason insufficient_balance) |
sitemap.completed | A sitemap job finishes |
sitemap.failed | A sitemap job fails terminally |
Use this header to dispatch events without inspecting the body shape:
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhooks/bytekit", methods=["POST"])
def handle():
event = request.headers.get("X-ByteKit-Event")
if event == "bulk.completed":
handle_bulk(request.json)
elif event == "scrape.completed":
handle_scrape(request.json)
return "", 200X-ByteKit-Event is a reserved header — you cannot supply it via webhook_headers in
monitor or bulk configuration. Attempts to do so will be rejected with a 422 at request
time.
Next steps
- Rate Limits — usage counting and concurrency slots
- Errors — how to interpret and retry error responses
- API Reference: Scrape — full request/response schema