Observation & data
Read a page into the JSON shape you asked for
Structured extraction against a live, rendered page — by field list, by JSON schema, or per row of a repeating listing, with a deterministic zero-LLM path when you can name the selectors.
extract
What it is
The job: Get data off a page in the shape your code already expects.
Give extract a URL and a shape — an array of field names, a { name: description } map, or a JSON schema — and it navigates, renders, reads and returns JSON in that shape. Because it runs a real browser, it reads what the page actually displays rather than what the server first sent.
Listings are where the cost lives, and where extract has the most to offer. Shipping a fifty-row page to a model is expensive and lossy. Pass `rowSelector` (CSS for ONE row or card) and the engine expands that element into every structurally-alike sibling and extracts per row; pass `rowPrompt` to describe a row in words instead, at the cost of one model call to resolve the selector. Add `rowFields` — { field: selectorRelativeToTheRow } — and the read becomes fully deterministic: an anchor yields its href, an image its src, anything else its trimmed text, and no model runs at all.
There is also a template path. Per-host and generic-metadata templates cover a lot of the web deterministically; when one matches and you have not forced a shape, the response carries `template` and the LLM cost is zero. GET /api/v1/templates lists the catalog and reports which template matches a given URL.
One useful escape hatch: `captureXhr` collects the page’s own background fetch/XHR responses whose URL matches a pattern. That is often the site’s own JSON API — cleaner and cheaper to read than its rendered DOM.
The call
Call it exactly like this.
Copied from the tool's registration and the route handler — not paraphrased.
// MCP tool call — server "twin-browser"
{
"tool": "extract",
"arguments": {
"url": "https://example.com/products",
"rowSelector": ".product-card",
"rowFields": {
"name": "h3",
"price": ".price",
"link": "a"
},
"maxRows": 200
}
}curl -X POST https://twin-browser.com/api/v1/extract \
-H "Authorization: Bearer $TWIN_API_KEY" \
-H "content-type: application/json" \
-d '{"url":"https://example.com/products","rowSelector":".product-card","rowFields":{"name":"h3","price":".price","link":"a"},"maxRows":200}'| Parameter | Type | What it does |
|---|---|---|
| url* | string | The page to extract from. Its origin must be a well-formed http(s) target. |
| fields | array | object | What to extract: an array of names, an array of { name, description, type }, or an object map { name: description }. |
| schema | object | A JSON Schema the output should conform to — use instead of fields. |
| prompt | string | Extra instruction to guide extraction. |
| rowSelector | string | CSS selector for ONE row of a repeating listing. Expanded to every structurally-alike element, then extracted per row. The response carries rowsFound. |
| rowPrompt | string | Describe one row in words instead of selecting it ("a product card"). Costs one extra model call to resolve a selector. Ignored when rowSelector is given. |
| rowFields | object | Per-row { fieldName: cssSelectorRelativeToTheRow }, read deterministically off each row — zero LLM. Returns data as an array, one object per row. |
| maxRows | number | Cap on expanded rows. Default 100, hard ceiling 500. |
| captureXhr | string | Collect the page’s own background XHR/fetch responses whose URL matches this pattern (substring, * wildcard, or /regex/) and return them as capturedXhr. |
| blockedDomains | string[] | Never fetch these hosts, subdomains included. Anti-bot, CAPTCHA and auth vendors are always allowed regardless. |
| blockAds | boolean | Also abort known ad/analytics/tracker hosts — faster and cheaper, but leave it off on guarded targets where their absence is itself unusual. |
| waitMs | number | Settle wait in milliseconds after load, before reading. |
| proxy | string | Your own egress proxy URL. |
Returns
{
"data": [ { "name": "…", "price": "…", "link": "https://…" } ],
"url": "https://example.com/products",
"status": 200,
"title": "Products",
"rowsFound": 48,
"template": "…", // present when a deterministic template supplied the result
"capturedXhr": [ … ], // present when captureXhr matched
"llmCostMicro": 0,
"credits_charged": 5
}What it costs
5 credits
A 5-credit floor, settled higher-of against the metered model cost — so a big schema over a long page bills what it consumed. The deterministic paths (rowFields, or a matching extraction template) run no model at all, so they settle at the floor with llmCostMicro of zero.
See the full rate cardWhich one
When a different tool is the right call.
The honest answer is often the neighbouring tool. These are the trades.
observe_pageobserve_page is cheaper and returns structure rather than answers. Use it when you want to decide what to do; use extract when you want the data itself.
observe_pagesubmit_crawlextract reads one page. A crawl reads a whole site and can run this same extraction per page — the same shape spec, applied across the BFS.
submit_crawletletl is extract plus chunking, embedding and persistence into a queryable store. If you are going to ask questions of the content later, ingest it with etl instead of extracting it and building your own index.
etlQuestions
extract, answered.
- How do I extract a listing without paying for the whole page?
- Point rowSelector at one row and let the engine expand it into every structurally-alike element on the page — matched on depth, tag, parent and grandparent tag plus attribute similarity. Extraction then runs per row instead of over the whole document. Add rowFields with a selector per field and no model runs at all.
- I passed no fields and no schema and got a 400. Why?
- Because there was nothing to extract into and no template matched the URL. Running a model with no shape at all can only guess from body text, so the engine refuses rather than returning something unparseable. Pass fields or a schema, or check GET /api/v1/templates to see whether a template covers the host.
- What is captureXhr for?
- Many pages render from their own JSON API. Instead of reading the rendered DOM, captureXhr collects the background responses whose URL matches your pattern and returns them as capturedXhr — usually cleaner, always cheaper, and immune to layout changes that would break a selector.
Keep going
The rest of the tool set.
observe_pageLet an agent see what is on a page before deciding what to do about it.
submit_crawlRead a defined slice of a site, once, without either missing it or over-reading it.
etlTurn web content into something you can ask questions of, without building a pipeline.
searchFind pages, and optionally have them read for you, in one call.