Guides

Scraping

Scrape any URL as HTML, markdown, or structured content — formats, options, latency, and async polling.

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:

OptionWhen it adds latency
cookiesNon-empty array
headersNon-empty object
delay_msGreater than 0
asynctrue (request is queued and processed off the fast path)

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 and Recording endpoints. Sending them to /v1/scrape returns 422.

# 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
  }'

Response fields

The default format is raw_html — pass formats to request others (markdown, html, links, images). The formats object in the response is keyed by the format you requested.

FieldDescription
formats.raw_htmlPage content in the requested format (raw_html is the default when formats is omitted)
contentLengthCompressed wire bytes — used for bandwidth billing
statusCodeHTTP status code of the origin page
metadata.titlePage <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 typeTrigger
bulk.completedA bulk job finishes (all items processed)
scrape.completedAn async scrape succeeds
scrape.failedAn async scrape fails terminally
monitor.change_detectedA monitor fires and detects a change (screenshot or content)
monitor.capturedA monitor fires with no change detected (notify_on: every)
sitemap.completedA sitemap job finishes
sitemap.failedA 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 "", 200

X-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