Extract
Turn one or more pages into JSON that matches your schema, or answer a question about them.
POST /v1/extract turns web pages into JSON. It opens up to 10 pages in a real browser, then an AI model fills your JSON Schema, follows your prompt, or both. The model reads each page as Markdown, like fetch gives it: links and images are absolute addresses, so a link or image address it returns works as it is, and heading permalinks are left out.
Request
curl -X POST https://api.boxline.dev/v1/extract \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d '{ "url": "https://example.com/pricing", "prompt": "The name and monthly price of every plan", "schema": { "type": "object", "properties": { "plans": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "price": { "type": "string" } } } } } } }'type Plans = { plans: { name: string; price: string }[] };const { data, usage } = await bx.extract<Plans>({ url: "https://example.com/pricing", prompt: "The name and monthly price of every plan", schema: { type: "object", properties: { plans: { type: "array", items: { type: "object", properties: { name: { type: "string" }, price: { type: "string" } } } } } },});console.log(data.plans, usage.costUsd);result = bx.extract( url="https://example.com/pricing", prompt="The name and monthly price of every plan", schema={ "type": "object", "properties": { "plans": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "price": { "type": "string" } } } } } },)print(result["data"]["plans"], result["usage"]["costUsd"])# schema.json holds the JSON Schema above.boxline extract https://example.com/pricing --prompt "The name and monthly price of every plan" --schema schema.json| Field | Default | Description |
|---|---|---|
url or urls | required | One page, or a list of 1 to 10. The model sees them all at once. With several, a page that does not load is listed in the answer instead of failing the call: see Several pages. |
prompt | none | What to pull out, in plain words. Up to 5,000 characters. You need a prompt, a schema, or both. |
schema | none | A JSON Schema the result must follow. Up to 20,000 characters as JSON. |
provider | first configured | "anthropic" or "openai". |
model | a fast model | claude-haiku-5-5 (Anthropic) or gpt-6-luna (OpenAI) unless you name another from the model list. |
It also takes the options every web API shares:
| Field | Default | Description |
|---|---|---|
waitUntil | "load" | When the page counts as loaded: "load", "domcontentloaded", "networkidle" (no network traffic for a moment) or "commit". |
delayMs | 0 | Extra wait after the page loads, up to 10,000 ms. Useful for pages that draw late. |
timeoutMs | 30000 | How long to wait for the page, from 1,000 to 120,000 ms. |
proxy | none | Load the page through a residential, datacenter or your own proxy, e.g. { "type": "residential", "country": "FR" }. The IP changes with every request unless you ask for "ip": "sticky". See Proxies. |
browser | standard | The page’s clock and language, e.g. { "mode": "realistic" } to follow the proxy’s country, or { "locale": "fr-FR", "timezone": "Europe/Paris" }. See Browser mode. |
blockAds | false | Refuse requests to ad and tracker sites while the page loads: faster, with less clutter, and no proxy data spent on them. See Ad and cookie-banner blocking. |
Response
{ "data": { "plans": [{ "name": "Pro", "price": "$20" }] }, "pages": [{ "url": "https://example.com/pricing", "finalUrl": "https://example.com/pricing", "status": 200, "title": "Pricing" }], "provider": "anthropic", "model": "claude-haiku-5-5", "usage": { "inputTokens": 5120, "outputTokens": 96, "costUsd": 0.0056 }, "ms": 4210}datais the result, shaped by your schema. The model usesnull(or leaves a field out) when the pages do not say.pagessays what each URL loaded as, in the order you gave them: the address you asked for, where it ended up, its HTTP status and its title.usage.costUsdis the model cost of this request.
Several pages
With several urls, one page that does not load does not fail the call. A page that could not be loaded (page_unreachable, page_timeout) or is a PDF (not_a_web_page) is left out of what the model reads, and is listed in pages with status: null, finalUrl: null and an error:
{ "data": { "plans": [{ "name": "Pro", "price": "$20" }] }, "pages": [ { "url": "https://example.com/pricing", "finalUrl": "https://example.com/pricing", "status": 200, "title": "Pricing" }, { "url": "https://status.example.invalid/", "finalUrl": null, "status": null, "title": "", "error": { "code": "page_unreachable", "message": "could not load status.example.invalid: ERR_TUNNEL_CONNECTION_FAILED (the name does not resolve (DNS))" } } ], "provider": "anthropic", "model": "claude-haiku-5-5", "usage": { "inputTokens": 2630, "outputTokens": 96, "costUsd": 0.0031 }, "ms": 5300}- The call fails only when no page loads. Then you get the first page’s error, as one page would (
502,504or422). Any other kind of error still fails the call. - With one
url, its error is the call’s error:502 page_unreachable,504 page_timeoutor422 not_a_web_page. - The model only sees the pages that loaded, and you pay for those only. Check
pagesforerrorbefore you trust a result that should cover every page.
const result = await bx.extract({ urls: ["https://example.com/pricing", "https://example.org/pricing"], prompt: "The name and monthly price of every plan",});for (const page of result.pages) { if (page.error) console.log("not read:", page.url, page.error.code, page.error.message);}result = bx.extract( urls=["https://example.com/pricing", "https://example.org/pricing"], prompt="The name and monthly price of every plan",)for page in result["pages"]: if page.get("error"): print("not read:", page["url"], page["error"]["code"], page["error"]["message"])Page text is data
Pages are treated as data, never as instructions: the model is told to ignore any instructions a page contains. Still, check the result before you act on it, as you would with any text from the web.
Cost and limits
- Each request is billed for its model use. See Pricing.
- Model use counts toward your plan’s monthly spend cap. Past it you get
402 spend_limituntil the 1st of the month (UTC). Extract on your own model key is not charged by us and is outside that cap. The Free plan runs models on your own keys only. See Your own model keys. - 30 requests per minute per project. Your plan must include extract (
402 feature_not_in_plan).
Errors
| Status and code | What it means |
|---|---|
502 page_unreachable | The page could not be loaded. The message names the host and Chrome’s reason, e.g. could not load example.com: ERR_NAME_NOT_RESOLVED. When Chrome’s reason is a generic one, the message adds the real one in words, e.g. (the name does not resolve (DNS)). See why a page did not load. |
504 page_timeout | The page did not finish loading within timeoutMs. Try a longer time or another waitUntil. |
422 not_a_web_page | The address is a PDF. Chrome only displays it, so there is no page text. Download it in a session (with the files API, or curl in its shell) and read it there, for example with pdftotext. The SDKs raise NotAWebPageError and never retry it. |
402 feature_not_in_plan | Your plan does not include this API, or the kind of proxy you asked for. |
429 rate_limited | Too many requests. Wait for the seconds in Retry-After. See Rate limits. |
The model can also fail: 422 model_refused when it declines, 502 extraction_failed when it returns no usable data. To pull data from a page you are already on in a session, use the extract step instead.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.