Scrape
Fetch one URL and get it back as clean Markdown, HTML, plain text, links, images, a screenshot or structured data. Scrape is synchronous: the response carries the page. Use it when you need a single page now; use Batch scrape for lists of URLs and Crawl to follow links across a site.
Requires an API key with the scrape scope. A scrape times out after 30 seconds, or 3 minutes when audio is requested; browser-backed scrapes allow 45 seconds per step. HTML, PDF, DOCX, XLSX and plain-text responses up to 50 MB are converted.
curl -X POST "https://xdive.dev/api/orgs/$ORG_ID/scrape" \
-H "X-API-Key: $XDIVE_API_KEY" -H "Content-Type: application/json" \
-d '{
"url": "https://example.com/pricing",
"formats": ["markdown", "links"],
"max_age": 3600000
}'from xdive import XDive
xdive = XDive() # reads XDIVE_API_KEY
page = xdive.scrape(
"https://example.com/pricing",
formats=["markdown", "links"],
max_age=3_600_000,
)
print(page["artifacts"]["markdown"]["content"])import { XDive } from "xdive";
const xdive = new XDive(); // reads XDIVE_API_KEY
const page = await xdive.scrape("https://example.com/pricing", {
formats: ["markdown", "links"],
max_age: 3_600_000,
});
console.log(page.artifacts?.markdown?.content);The SDKs return the data object of the response. They look up your organization id once through GET /whoami, so you only configure the key.
Parameters
| Field | Type | Default | Description |
|---|---|---|---|
url | string | required | Absolute http or https URL to fetch. |
formats | array | ["markdown"] | Representations to return. Each entry is a format name or an object with per-format options. See Formats. |
extract | boolean | true | Main-content extraction: drop navigation, headers, footers and sidebars. false converts the whole page. |
respect_robots | boolean | true | Refuse URLs that the site's robots.txt disallows for the crawler. |
max_age | integer | 0 | Freshness window in milliseconds. A cached copy younger than this is served without a network fetch. 0 uses the server's default window. See Caching. |
max_chars | integer | 0 | Return at most this many characters of Markdown, cut at a paragraph break. 0 means no limit; otherwise at least 500. See Long pages. |
offset | integer | 0 | Continue a long page from the previous response's next_offset. |
store_in_cache | boolean | true | false bypasses the page cache for this request: the page is fetched live and not stored. |
headers | object | none | Extra request headers for the page fetch, such as Accept-Language, a mobile User-Agent or Cookie. See Custom headers. |
actions | array | none | Browser steps to run before the content is captured (click, fill, press, scroll, wait). At most 15. See Browser actions. |
wait_for | integer | 0 | Milliseconds to wait after the page loads and the actions run, before capture. 0 to 10000. Uses the browser. |
full_page_screenshot | boolean | false | With the screenshot format, capture the full scrollable page instead of the viewport. |
redact_pii | boolean | false | Replace emails, phone numbers, card numbers, SSNs, IBANs and IP addresses with typed tokens. See Privacy controls. |
zero_data_retention | boolean | false | Do not write the page to the cache and mark the response no-store. |
lockdown | boolean | false | Serve only an already-cached copy and make no outbound page fetch. Implies zero_data_retention. |
Formats
Pass format names as strings, or objects when a format takes options. Each format may appear once. Names are case-insensitive and a few aliases are accepted (md, txt, raw_html, clean_html, summarize, answer, screenshots, mp3); the response always uses the canonical name.
| Format | Returned in the artifact | Notes |
|---|---|---|
markdown | content | Clean Markdown. The default. |
text | content | Plain text. |
html | content | Clean HTML rendered from the parsed page (main content when extract is on). |
rawHtml | content | The HTML as fetched. |
links | links: list of url and text | Every link on the page, whatever extract is set to. |
images | images: list of url, alt, title | Every image on the page. |
branding | branding | Observed identity signals: name, description, logo, favicon, colors, fonts, css_variables. Absent values stay absent. |
summary | content | A short summary: 3 to 6 sentences written by your LLM when one is configured, otherwise 5 sentences extracted from the page. Accepts prompt for extra instructions. |
question | content | Answers question (or prompt) from the page only. Uses your LLM when configured, otherwise the most relevant sentences. |
highlights | highlights: list of text and score | Sentences most relevant to query (or prompt), best first. No LLM. max_results sets the count (default 5). |
json | json | Structured data from prompt and/or a JSON Schema in schema. Requires an LLM. |
product | json | Product fields (title, brand, price, currency, availability, sku, variants…) from a built-in schema. Requires an LLM. |
menu | json | Restaurant menu sections and items from a built-in schema. Requires an LLM. |
screenshot | content | A PNG as a data:image/png;base64,… URL, at most 8 MiB. Runs in the browser. See Screenshots. |
audio | url | The audio track of an HTTPS YouTube or Vimeo URL as MP3, up to 100 MB. See Audio. |
The LLM is the model configured for your organization in AI settings (or the platform's). Without one, json, product and menu fail with 501. Text sent to the model is capped at 120,000 characters.
Format options
| Option | Type | Used by | Description |
|---|---|---|---|
type | string | all | The format name. |
prompt | string | json, summary, question, highlights | Instructions for the format. For question and highlights it stands in for question and query. |
schema | object | json | A JSON Schema for the result. Must be valid JSON. |
question | string | question | The question to answer. question or prompt is required. |
query | string | highlights | What the highlights should be relevant to. Without it, the page's most representative sentences are returned. |
max_results | integer | highlights | Number of highlights, 0 to 50. |
{
"url": "https://example.com/product/espresso-grinder",
"formats": [
"markdown",
{ "type": "json", "prompt": "Extract the product", "schema": {
"type": "object",
"properties": { "name": { "type": "string" }, "price": { "type": "number" } }
} },
{ "type": "highlights", "query": "warranty", "max_results": 3 }
]
}Screenshots
Requesting screenshot routes the scrape through an isolated, short-lived browser session instead of the standard fetch. The other formats you ask for are rendered from the same final DOM. Add full_page_screenshot: true for the whole page.
Audio
audio extracts the audio of an HTTPS YouTube (youtube.com, youtu.be) or Vimeo URL. The artifact's url is a signed link to the MP3 that expires after one hour. Audio needs object storage on the server, cannot be combined with zero_data_retention or lockdown, and is not available on crawls or batch scrapes. If extraction fails the request returns 422.
Main-content extraction
With extract on (the default), the page's main article is found and navigation, headers, footers and sidebars are dropped before conversion, so markdown, text and html hold the content a reader came for. Set extract: false to convert the whole document. Metadata (title, description, lang…), links and images always describe the whole page.
Caching and max_age
Pages are cached across the platform. On a fresh hit the response is served without a network fetch and the page reports fetch_route: "cache" with fetch_cost_units: 0.
max_agesets how old a cached copy may be, in milliseconds. An older copy is fetched again from the site, and the fresh response replaces it. A window longer than the server default accepts older copies for as long as the cache still holds them.store_in_cache: falseskips the cache in both directions for this request.zero_data_retentionandlockdownnever write to the cache.- Responses fetched with custom
headersare cached separately from header-less fetches, so a page fetched with your cookies is never served to a request without them.
Robots.txt
By default the crawler checks the site's robots.txt and refuses disallowed URLs with 403 and disallowed by robots.txt. Set respect_robots: false to skip the check when you have the right to fetch the page.
Long pages
Every Markdown artifact reports estimated_tokens, about four characters per token, so an agent knows what a page costs to read before it reads it. To read a long page in parts, pass max_chars. The artifact then carries total_chars and, while more remains, next_offset. Send the same request again with offset set to next_offset to get the next part. A continuation reuses the copy fetched for the first part (up to 10 minutes old), so all parts come from the same fetch. Each request is metered as one page.
{ "url": "https://go.dev/doc/effective_go", "max_chars": 4000 }"markdown": { "content": "# Effective Go …", "estimated_tokens": 915, "total_chars": 102176, "next_offset": 3661 }JavaScript pages and warnings
A plain scrape starts with a fast HTTP fetch. When the result is a JavaScript application shell or loses most of the page's text, the server renders the page again in a browser and returns that instead; fetch_route is then browser and fetch_reason says why. The browser time is metered as browser seconds, on top of the page.
When the content still looks incomplete, for example because the browser quota is used up or the request sends custom headers (a browser cannot send them), the page succeeds with a warnings array explaining it. Retry with wait_for to give a slow page more time to render, or use what you got.
Custom headers
headers adds up to 10 request headers to the page fetch. Names are 1 to 128 characters, values at most 2048 and without line breaks. Host, Content-Length, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Proxy-Authorization and any X-Forwarded-* header are rejected. A User-Agent you set replaces the crawler's. Headers apply to the HTTP fetch; browser-backed scrapes do not send them.
Browser actions
actions, wait_for and the screenshot format run the scrape in an isolated, ephemeral browser session: it opens url, runs your actions in order, waits wait_for milliseconds, captures the DOM and renders the requested formats from it. Each action has 45 seconds to complete.
| Action | Fields | Description |
|---|---|---|
click | selector | Click the first element matching a CSS selector. |
fill | selector, value | Wait for the element to be visible, focus it and set its value. |
press | key | Press one of Enter, Tab, Escape, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Space. |
scroll | x, y | Scroll the window by this many pixels (each between -100000 and 100000). |
wait | selector or milliseconds | Wait until the selector is visible, or sleep 1 to 10000 milliseconds. |
Selectors are at most 1000 characters and values at most 10000. goto, extract and screenshot actions are not accepted on scrape: the scrape navigates to url itself, and you request content and screenshots through formats.
{
"url": "https://example.com/search",
"actions": [
{ "type": "fill", "selector": "input[name=q]", "value": "espresso" },
{ "type": "press", "key": "Enter" },
{ "type": "wait", "selector": ".results" }
],
"wait_for": 1000,
"formats": ["markdown", "screenshot"]
}Browser-backed scrapes are metered as browser seconds (rounded up, at least one) against your plan's browser quota instead of pages. They need browser automation on the server (501 otherwise) and a free browser session (409 when the server's sessions, or your plan's, are all in use). respect_robots applies to the starting URL (on by default; a disallowed URL returns 403). headers cannot be sent by the browser and are rejected with 400; max_age and store_in_cache do not apply, since a browser scrape always renders live. They cannot be combined with lockdown. They are not available through GET.
Privacy controls
redact_pii deterministically replaces machine-recognizable identifiers in the returned title, description and text artifacts (including json and highlights) with typed tokens: [REDACTED:email], [REDACTED:phone], [REDACTED:card] (Luhn-checked), [REDACTED:ssn], [REDACTED:iban] and [REDACTED:ip]. Names and street addresses are not detected. Link and image URLs are not changed.
zero_data_retention keeps the page out of the cache, sets Cache-Control: no-store, private and Pragma: no-cache, and the audit log entry for a standard scrape omits the URL. Formats that use an LLM still send the page text to your configured model.
lockdown serves the page only if it is already cached, whatever its age. No DNS lookup, robots.txt fetch, HTTP request or browser session is made. A cache miss returns 404 with SCRAPE_LOCKDOWN_CACHE_MISS.
Lockdown makes no model calls either. Request only formats rendered from the page (markdown, text, html, rawHtml, links, images, branding, highlights); asking for json, product or menu, or for summary or question when a model is configured, returns 400 before any work is done.
A platform administrator can make zero data retention or lockdown the default for every request. A successful POST reports the policy that applied in security, and both POST and GET responses carry two headers:
| Header | Values |
|---|---|
X-Data-Retention | none with zero data retention, otherwise standard |
X-Network-Policy | cache_only under lockdown, otherwise live |
Response
{
"success": true,
"data": {
"url": "https://example.com/pricing",
"title": "Pricing",
"description": "Plans for teams of every size.",
"lang": "en",
"depth": 0,
"status_code": 200,
"word_count": 412,
"fetched_at": "2026-10-15T09:21:44.118302Z",
"fetch_route": "impersonate",
"fetch_attempts": 2,
"fetch_cost_units": 3,
"fetch_reason": "blocked or transient HTTP status 403",
"artifacts": {
"markdown": {
"media_type": "text/markdown; charset=utf-8",
"content": "# Pricing\n\nStart free, then pay as you grow...",
"bytes": 2841,
"quality": { "passed": true, "score": 100 }
},
"links": {
"media_type": "application/json",
"links": [{ "url": "https://example.com/docs", "text": "Docs" }],
"bytes": 96,
"quality": { "passed": true, "score": 100 }
}
},
"links": ["https://example.com/docs", "https://example.com/signup"]
},
"security": {
"zero_data_retention": false,
"network_policy": "live",
"cache_state": "eligible",
"outbound_permitted": true
}
}| Field | Type | Description |
|---|---|---|
success | boolean | true on success. |
data | object | The page. |
security | object | The data policy that applied to this request. |
The page object
| Field | Type | Description |
|---|---|---|
url | string | Final URL after redirects. |
title, description, lang, canonical, image, site_name | string | Page metadata, when present. |
depth | integer | Always 0 for a scrape. |
status_code | integer | HTTP status of the fetch. |
word_count | integer | Words in the page's Markdown. |
fetched_at | string | When the content was fetched (RFC 3339). |
fetch_route, fetch_attempts, fetch_cost_units, fetch_reason | How the page was fetched. See Fetch telemetry. | |
artifacts | object | One artifact per requested format, keyed by format name. Only requested formats are returned. |
links | array of strings | URLs of the links found on the page. |
Each artifact has media_type, bytes, a quality report (passed, score from 0 to 100, optional warnings) and the field for its format: content, json, links, images, highlights, branding or url.
Fetch telemetry
Each page reports how it was fetched. A plain HTTP request is tried first; on a block, challenge or bot-filter response the fetch escalates to a Chrome TLS fingerprint, and on servers with an egress proxy, sites that refuse the server's network are retried through the proxy.
| Field | Description |
|---|---|
fetch_route | The route that produced the content: cache, http, impersonate, browser or proxy. |
fetch_attempts | Network attempts made, across routes. 0 for a cache hit. |
fetch_cost_units | Relative cost of the attempts: 1 per HTTP attempt, 2 per impersonated attempt, 20 per browser render, plus 5 for the egress proxy. 0 for a cache hit. It is telemetry, not your bill: a scrape is metered as one page. |
fetch_reason | Why the fetch escalated or where it came from, for example blocked or transient HTTP status 403, JavaScript application shell detected or fresh response cache hit. Empty when the first HTTP attempt succeeded. |
The security object
| Field | Values |
|---|---|
zero_data_retention | Whether zero data retention applied. |
network_policy | live, or cache_only under lockdown. |
cache_state | eligible (the page may be cached), write_disabled (it was not stored), or hit (lockdown served the cache). |
outbound_permitted | false under lockdown. |
Government and banking sites
Many government and banking sites refuse every connection from data-centre networks, so no fetch strategy reaches them from the crawler's own address. XDive can route those requests through a residential proxy. The proxy is paid per page, so it is off for every organization until the platform administrator enables it for yours, with a monthly allowance of proxied pages (200 by default). Your current allowance and use are in the proxy object of GET /orgs/{id}/usage.
With access enabled, scrapes, crawls and batch scrapes go direct first and use the proxy only for sites that cannot be reached or stay blocked. Proxied pages report fetch_route: "proxy", and each one counts once against the allowance. When the allowance is used up, fetches go direct only until the next calendar month.
Two errors are specific to these sites:
403 PROXY_NOT_ENABLED: the site blocks data-centre networks, and your organization has no proxy access or has used its allowance. Ask the platform administrator to enable the proxy, or to raise the allowance.403 SITE_RESTRICTED: the proxy is enabled for your organization, but the proxy provider refused the destination. Providers typically refuse government and banking sites until the platform's proxy account is identity-verified. Retrying will not help.
In crawl and batch results, the same codes appear as the failed page's error_code.
GET (Markdown only)
The original endpoint returns the page's Markdown as text/markdown instead of JSON. It accepts the query parameters url, respect_robots, zero_data_retention and lockdown, and the same errors as POST. Formats, actions, headers and max_age need POST.
curl "https://xdive.dev/api/orgs/$ORG_ID/scrape?url=https://example.com" \
-H "X-API-Key: $XDIVE_API_KEY"Errors
Errors have an error message; the ones below that carry a code also have "success": false.
| Status | Code | When |
|---|---|---|
400 | Missing url, an unknown or repeated format, invalid format options, an invalid header, a negative max_age, more than 15 actions, wait_for out of range, or an option combination that is not allowed. | |
402 | The monthly page quota (or browser quota, for browser scrapes) is used up. | |
403 | The key lacks the scrape scope, or robots.txt disallows the URL. | |
403 | PROXY_NOT_ENABLED | The site blocks data-centre networks and your organization has no residential proxy access, or has used its monthly proxy allowance. See Government and banking sites. |
403 | SITE_RESTRICTED | Your organization may use the residential proxy, but the proxy provider refused this destination, which is common for government and banking sites. Retrying will not help. |
404 | SCRAPE_LOCKDOWN_CACHE_MISS | Lockdown is on and the page is not cached. |
409 | No browser session is free for a browser-backed scrape. | |
422 | Audio extraction failed or the URL is not a supported video. | |
429 | Your plan's request rate limit was exceeded. Retry after Retry-After. | |
429 | CONCURRENCY_LIMIT | Too many concurrent requests for your plan. Retry after Retry-After. |
501 | A requested format needs an LLM that is not configured, or the server has no browser automation. | |
502 | The page could not be fetched or converted (scrape failed: …), or an enrichment format failed. | |
503 | SERVER_BUSY | The server is at capacity. Retry after Retry-After. |
See Errors and limits for authentication errors, rate limits and retries.
Related
- Batch scrape to scrape up to 100 URLs as one job.
- Crawl to follow links across a site.
- Extract for structured data across several pages.
- Usage and billing for what is metered.