POST /v1/read. What you pass in the body determines whether Reader runs a synchronous scrape, a batch job, or a crawl.
Three shapes of input
| You send | Reader does | You get back |
|---|---|---|
url: "..." | Scrape that one URL synchronously | { kind: "scrape", data } with markdown in-line |
urls: [...] (one or more) | Batch job that scrapes every URL async | { kind: "job", data } with job ID to poll |
url + maxPages or maxDepth | Crawl the site starting from url | { kind: "job", data } with job ID to poll |
Why one endpoint
You learn one contract instead of four. Your code branches on what it sent, not on which URL it called. When you want to swap a batch for a crawl, you change the body; the endpoint, auth, error handling, retry logic, and response envelope all stay the same.Synchronous scrape
Async batch or crawl
id to poll GET /v1/jobs/{id}, stream progress with SSE, or subscribe a webhook for completion. See Async jobs.
What Reader decides for you
You tell Reader what to fetch. Reader decides how:- How to render the page (full browser with JavaScript execution and TLS fingerprinting).
- Whether to escalate the proxy from datacenter to residential when a block is detected (see Proxy modes).
- Whether to serve from cache.
- How to parallelize a batch.
Response envelope
Every JSON response from/v1/read follows the same envelope:
Next
- Scrape vs crawl: when to pick which mode
- Proxy modes:
standard,stealth, andauto - Async jobs: poll, stream, or webhook-notify

