# Structured Web Extractor - JSON Schema with Evidence (`leadproof/structured-web-extractor`) Actor

Turn public web pages into records that match your JSON Schema. Every value carries its evidence: the JSON-LD path or the exact quote it came from. Values without evidence are rejected, and required facts that are missing leave the record partial.

- **URL**: https://apify.com/leadproof/structured-web-extractor.md
- **Developed by:** [Lead Proof](https://apify.com/leadproof) (community)
- **Categories:** AI, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 extracted records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What does Structured Web Extractor do?

Structured Web Extractor turns **public web pages into records that match your JSON Schema**, and every value
comes with **field-level evidence**: the JSON-LD path it was read from, or the exact quote from the page that
contains it. Values that cannot be traced back to the page are **rejected and listed**, never silently kept.
Required fields that are missing leave the record **partial** instead of being filled with a guess.

Give it page URLs, page text you already have, or a dataset of page records. Choose a built-in schema
(product, local business) or bring your own. Run it on Apify with API access, scheduling and integrations.

Extraction is not a guarantee of factual correctness: a page can be wrong, stale or ambiguous, and a value can
be on the page without belonging to the business or product you care about. The evidence shows exactly what
the page says, so you can check it; it is not proof that the fact is true.

### Why use it?

- **Auditable data**: each value carries `sourceUrl`, `method` and either `recordPath` + `observed` (structured
  data) or `quote` (text), and says whether the value itself is on the page (`basis: source`) or is the model's
  reading of a quote (`basis: interpretation`). Interpretations are kept out of `data` unless you ask for them.
- **No silent guessing**: missing facts stay absent; conflicting facts are reported in `conflicts` and left
  out of `data`; unverifiable AI output goes to `rejected` with a reason.
- **Your schema, validated**: output is checked against your schema; `validation.missingRequired` lists what
  the page did not support.
- **No AI cost unless you ask for it**: the default mode is deterministic (JSON-LD, metadata, email and phone
  patterns). AI extraction is opt-in and capped by a per-run budget.

### How to use it

1. Pick a **Source**: page URLs, records (text/HTML/PageRecords you provide) or an Apify dataset.
2. Pick a **Schema preset** or choose *custom* and paste your **Extraction schema**.
3. Pick a **mode**: *deterministic* (default, no AI), *llm*, or *auto* (deterministic first, AI only for fields
   still empty).
4. Run. Read the **Results** view, the **Evidence and rejections** view, and the `SUMMARY` record.

### Examples

| Example | What it shows |
|---|---|
| **Business contact details from company websites** | `local-business` preset on three company sites: name, emails, phones, website and postal address, each value backed by the JSON-LD path it came from. Publisher contacts are rejected. No AI cost. |
| **Any fields you define, from pages without markup** | A custom schema filled by AI on pages with no structured data, where every value must quote the page. `inStock` arrives as an interpretation, separate from the values found on the page. |

Both run on the Examples tab with their inputs prefilled.

### Input

| Field | Meaning |
|---|---|
| `sourceMode` | `urls` (default), `records` or `dataset`. Only the selected source is used. |
| `urls` | Public http(s) pages. One static-HTML request each (no browser, no proxy). |
| `records` | Objects with `url` (absolute; used as the evidence source) and `text`, `markdown`, `html` (up to 2 MB) or `structuredData`; optional `inputId`, `title`. PageRecord v1 objects are accepted as they are. |
| `datasetId`, `datasetOffset` | Dataset of PageRecord v1 items (for example a page reader's output). Declared as a dataset resource with READ permission. Rows keep the dataset's order and `inputId`s. |
| `schemaPreset` | `product`, `local-business` or `custom`. |
| `extractionSchema` | Your schema (see below). Used when the preset is `custom`. |
| `mode` | `deterministic`, `llm` or `auto`. |
| `llmProvider` | `apify-openrouter` (default; billed to the Apify account running the Actor) or `openrouter` (your own key). |
| `llmModel` | OpenRouter model ID with structured output and a fixed price. Default `openai/gpt-4.1-mini`. |
| `llmApiKey` | Secret field, stored encrypted by Apify. Only for `openrouter`. |
| `maxLlmCostUsd` | LLM budget per run, default 0.25 USD (0.01-20). |
| `maxInputChars`, `maxOutputTokens` | Model input and output bounds (default 20,000 characters and 1,500 tokens). |
| `interpretedValues` | `separate` (default): interpretations are listed in `interpretations`, not in `data`. `include`: also in `data`, still listed. |
| `recordTimeoutSecs` | Time limit per record, default 120 s (20-600), covering reading, extraction and model retries. |
| `maxRecords`, `concurrency`, `requestTimeoutSecs`, `maxPageBytes` | Hard limits, enforced for API input too. |
| `respectRobotsTxt` | Default true (RFC 9309, wildcards included). |
| `saveSourceRecords` | Store each source PageRecord as `PAGE-<inputId>` so every `recordPath` can be audited. |
| `readerMaxTotalChargeUsd` | Maximum the reader Actor may charge in its own run, default $1. Separate from this run's maximum charge. |
| `readerActorId`, `readerUrlsField` | Read URLs through a PageRecord v1 reader Actor (for example `leadproof/web-page-reader`) instead of the built-in fetcher. It runs as a separate run on your account. `respectRobotsTxt` is still enforced by this Actor before any URL is handed over. The reader's run ID is stored, so a resumed run re-attaches to it instead of reading again. |

Example (the Console prefill):

```json
{"sourceMode": "urls", "urls": ["https://web-scraping.dev/product/1"], "schemaPreset": "product"}
```

### Supported schema

The schema language is a **closed subset of JSON Schema 2020-12**. Anything outside it is refused before any
page is read, with the keyword and location in the error.

- Root: `{"type": "object", "properties": {...}}`. Objects are always closed (`additionalProperties: false`).
- Types: `string`, `number`, `integer`, `boolean`, `object`, `array`, and nullable pairs such as
  `["string", "null"]`. Arrays take one `items` schema; arrays of arrays are not supported.
- Keywords: `properties`, `required`, `items`, `minItems`, `maxItems` (0-100, default cap 25), `enum`
  (up to 50 scalar values), `format` (`email`, `uri`, `date`, `date-time`), `minimum`, `maximum`,
  `minLength`, `maxLength`, `title`, `description`, `x-extract`, and a top-level `$schema` (ignored).
- Refused, with a reason: `$ref`/`$defs` (no remote or shared definitions), `anyOf`/`oneOf`/`allOf`/`not`/
  `if`, `pattern`/`patternProperties` (no user regular expressions), `default`/`const`/`examples` (they would
  put values in the output that the page never stated), any other keyword or `x-` extension.
- Limits: 16 KB serialized, 5 levels of object/array nesting, 80 properties, 60 scalar fields, property names
  `^[A-Za-z][A-Za-z0-9_-]{0,63}$`.

**Missing facts** are absent from `data` (the key is not present), in every mode. They are never `null`-filled
or defaulted. A missing `required` field makes `validation.valid` false and the row `partial`.

#### Deterministic bindings (`x-extract`)

Deterministic mode fills a field only when the schema binds it; it never guesses from field names. Unbound
fields are listed in `unboundFields`. A binding, or a list of up to 4 tried in order (the first that yields a
value wins):

| Binding | Reads |
|---|---|
| `jsonld:Product.offers.price` | JSON-LD entities of the type (with a small documented subtype table, for example LocalBusiness matches Restaurant and HardwareStore), then the property path. Arrays fan out. |
| `jsonld:LocalBusiness\|Organization.name` | Several types. |
| `jsonld:@.price` | Relative to the JSON-LD node matched by the enclosing array or object (nested offers, addresses, shipping rows). |
| `meta:title`, `meta:description`, `meta:og:price:amount`, ... | Page title, description, canonical URL, language and meta tags. |
| `common:email`, `common:phone` | Addresses and phone numbers in the page text (a phone needs a leading `+`/`(` or a phone word before it). |

### Output

One dataset row per input, in input order, including failed and skipped rows. Real row from run
`CF9kZekfMh5PNKlbw` (deterministic, `product` preset; some fields omitted and the description shortened here):

```json
{
  "schemaVersion": "1.0",
  "inputId": "u0000-e06a7bd5",
  "status": "succeeded",
  "sourceUrl": "https://web-scraping.dev/product/1",
  "mode": "deterministic",
  "data": {"name": "Box of Chocolate Candy", "brand": "web-scraping.dev", "price": 9.99, "priceCurrency": "USD",
           "availability": "InStock", "description": "Indulge your sweet tooth with...", "ratingValue": 4.7, "reviewCount": 10},
  "validation": {"valid": true, "errors": [], "missingRequired": []},
  "evidence": {
    "/price": [{"sourceUrl": "https://web-scraping.dev/product/1", "method": "jsonld",
                "recordPath": "/structuredData/jsonLd/0/offers/lowPrice", "observed": "9.99",
                "binding": "jsonld:Product.offers.lowPrice", "valueCheck": "structured"}]
  },
  "transformations": [{"pointer": "/availability", "raw": "https://schema.org/InStock", "value": "InStock",
                       "transformation": "schema_org_enum_shortened"}],
  "conflicts": [], "rejected": [], "coverage": {"fieldsWithValues": 8, "fieldsInSchema": 10},
  "methods": ["jsonld"], "model": null, "warnings": [], "error": null
}
```

Real LLM evidence from run `2V0aAINpczAu4ITr4` (custom business schema, `openai/gpt-4.1-mini` through the Apify
OpenRouter proxy):

```json
"evidence": {"/email": [{"method": "llm", "quote": "Reservations: reservas@casaazul-fixture.mx or +52 951 555 0142.",
                         "location": "text", "valueCheck": "literal", "sourceUrl": "https://casaazul-fixture.mx/"}]},
"model": {"provider": "apify-openrouter", "model": "openai/gpt-4.1-mini", "inputTokens": 670, "outputTokens": 206,
          "costUsd": 0.0005976, "attempts": 1, "repairs": 0}
```

You can download the dataset as JSON, CSV, Excel or HTML. `data` and `evidence` are nested objects, so JSON
keeps them intact.

| Field | Meaning |
|---|---|
| `status` | `succeeded`: schema-valid, every value evidenced. `partial`: a required field missing, conflicting values, or a model error with some values. `failed`: page unreadable or extraction error. `skipped`: not processed (LLM budget, source marked skipped). |
| `data` | Your schema's fields. |
| `evidence` | JSON Pointer of each value in `data` -> up to 3 entries: `sourceUrl`, `method` (`jsonld`, `metadata`, `text-pattern`, `llm`), `quote` or `recordPath` + `observed`, `valueCheck` (`structured`, `literal`, `numeric`, `numeric_ambiguous`, `interpreted`), `basis` (`source` or `interpretation`). |
| `interpretations` | Values that are the model's reading of a quote (booleans, enum values the page words differently), with their evidence and `inData`. |
| `rejected` | Values dropped and why: `quote_not_found`, `value_not_in_quote`, `quote_not_relevant`, `quote_contradicts_value`, `quote_does_not_support_value`, `missing_evidence`, `publisher_contact`, `reserved_domain`, `evidence_in_suspected_injection`, `violates_schema`, `not_in_enum`, `ambiguous_number`... |
| `conflicts` | Fields where the page states different values, each with evidence. |
| `transformations` | Every change from the observed value: `string_to_number`, `number_parsed_from_text`, `html_unescape`, `schema_org_enum_shortened`, `string_to_boolean`, `value_to_string`. Units and currencies are never converted. |
| `model` | Provider, endpoint, model, attempts, repairs, tokens and provider-reported cost; `null` when no model was called. |
| `timings` | Seconds per stage for the row: `readSeconds`, `deterministicSeconds`, `llmSeconds`, `totalSeconds`. |
| `SUMMARY` (key-value store) | Counts by status, stop reason, fetch statistics (including the reader run IDs), LLM calls, tokens, cost, latency, summed and maximum stage timings. |

#### What the evidence proves, and what it does not

- `basis: source`, `valueCheck: structured`: the value (after the labeled transformation) is at `recordPath` in
  the page's structured data. `literal` / `numeric`: the value is written in the quoted text.
- `basis: interpretation`, `valueCheck: interpreted`: the quote is on the page and the value is the model's
  reading of it, for example `inStock: true` from "In stock". Before it is kept, the quote must mention the field
  (a word of its name, title or description, inflections allowed), a `true` must not be negated ("not accepting
  new patients"), a `false` must be negated, and an enum quote must not name a different allowed value. These are
  lexical checks, not proof: they catch contradicting and off-topic quotes, not every misreading. Descriptions in
  the page's language help on non-English pages.
- `numeric_ambiguous`: the page's number reads two ways ("1.250" is 1250 or 1.25); the row warns `ambiguous_number`.
- No evidence type proves the fact is true, current, or about the right entity. JSON-LD entity scoping and the
  publisher-contact rules below reduce wrong attributions; a quote alone does not rule them out.

### How AI extraction is kept honest

- The model gets no tools and no credentials. The page is wrapped in a random per-request delimiter, and
  paragraphs that read as instructions to an AI are replaced before sending; the row gets
  `prompt_injection_suspected`, and any value whose quote lies in such text is rejected.
- Output must match a strict JSON schema. Invalid output gets exactly one repair request; output cut off at
  `maxOutputTokens` is not repaired.
- Every value needs an exact quote from the page (whitespace and typographic punctuation normalized) that
  contains the value, or, for booleans and reworded enum values, a quote that passes the interpretation checks
  above (these values are kept out of `data` by default). A quote the model gave for the enclosing list or a sibling field of the same item may
  support a value, under the same checks (`quoteFrom` shows it).
- Contacts that belong to the site publisher on a third-party listing (for example a directory's own email)
  are rejected as `publisher_contact`; placeholder addresses at `example.com`, `.example` or `.test` as
  `reserved_domain`.
- Before each call, its worst-case price (input estimate plus `maxOutputTokens`) is reserved against
  `maxLlmCostUsd`; calls that would exceed it are not sent and their rows are `skipped`.

### How much does it cost?

**Proposed price: $0.002 per extracted record**, charged only for a row that carries at least one value with valid
evidence. Rows that failed, were skipped, or came back empty are free, and a row whose only output is an
interpretation is free as well. Starting the Actor costs $0.00005. There is no per-dataset-item fee: the Actor
refuses to run if one is configured.

Model tokens and a reader Actor, when you use them, are billed by those Actors to the account that runs this one,
on top of the price above (see "Who pays for what").

**What 100 pages cost, by component** (only rows with a verified value are charged, so a batch with failures costs
less):

| | Extraction (this Actor) | Model | Page reader |
|---|---|---|---|
| Deterministic, built-in reader (default) | 100 x $0.002 = $0.20, plus $0.00005 to start | none | none |
| `auto` or `llm`, built-in reader | $0.20 | $0.05 to $0.14 measured with `openai/gpt-4.1-mini` ($0.0005 to $0.0014 per page) | none |
| Deterministic, through a reader Actor | $0.20 | none | $0.20: LeadProof's Web Page Reader charges $0.002 per page read |

Totals for 100 pages: $0.20 deterministic with the built-in reader, $0.25 to $0.34 with the model, $0.40 through
the reader Actor.

Each component has its own limit: the run's **maximum charge** bounds the extraction events, **`maxLlmCostUsd`**
bounds the model, and **`readerMaxTotalChargeUsd`** bounds the reader run. They are independent; one does not cover
another.

Costs measured on the owner account on 2026-09-19 (this Actor at 512 MB):

| | Measured |
|---|---|
| This Actor, deterministic, 1-6 URLs | $0.00035-0.00071 per run (5-17 s) |
| This Actor, LLM or auto mode, 2-3 pages | $0.0006-0.0008 per run (10-18 s); one outlier took 81 s ($0.0026) |
| Reader Actor child run (`leadproof/web-page-reader`, 1 GB, 2-5 static pages) | $0.00035-0.0005 per run |
| LLM tokens, `openai/gpt-4.1-mini`, typical product or business page | $0.0005-0.0014 per page (670-1,130 input tokens, 130-570 output tokens) |

#### Who pays for what

Apify runs an Actor "under the account associated with the provided token, so all consumed resources are charged to
this user account" (docs: Run Actors programmatically). This Actor starts the page reader and calls the OpenRouter
proxy with its own run token, so both land on the account that started this Actor, whoever that is.

| Cost | You start this Actor yourself | Another Actor starts it inside its own run | Basis |
|---|---|---|---|
| Extraction: this Actor's events ($0.002 per extracted record) | you | the account running the calling Actor | **verified** on the owner account: only rows with evidence were charged, and the run's maximum charge stopped the run at the limit. Cross-account: documented |
| Extraction: this Actor's compute and storage | the Actor owner under pay-per-event | the Actor owner under pay-per-event | documented (Apify's pay-per-event profit formula) |
| Model tokens, `apify-openrouter` (llm and auto modes) | you, billed by Apify's OpenRouter Actor | the account running the calling Actor | **verified** that the charge lands on the account that ran this Actor (owner account, standby run) and equals the provider-reported cost. Cross-account: documented |
| Model tokens, `openrouter` provider | your own OpenRouter key | the key supplied in that run's input | verified by design: the key is sent only to openrouter.ai |
| Page reader Actor (`readerActorId`) | you, at that Actor's price | the account running the calling Actor | **verified** that the child run belongs to the account that ran this Actor (owner account). Cross-account: documented |
| Built-in reader (default) | inside this Actor's compute, no other Actor is started | same | **verified** live |

The run's maximum charge setting covers this Actor's own events only. Model tokens and a reader run are charged by
other Actors, so they have their own bounds: `maxLlmCostUsd` and `readerMaxTotalChargeUsd`. Deterministic mode (the
default) calls no model and starts no reader Actor, so extraction is the only cost.

"Verified" above means measured on the owner account, where the caller and the owner are the same account. Which
account is billed when **someone else** runs it follows Apify's documentation and has not been observed yet; that
check is a launch task (`BILLING_VERIFICATION.md`).

### What you need to run it

| Dependency | Needed for | Access |
|---|---|---|
| An Apify account | every run | yours |
| `apify/openrouter` (public Actor) | `llm` and `auto` modes; the run's own token authenticates it | public on Apify Store, billed to your account, no signup or key |
| Your own OpenRouter key (optional) | `llm`/`auto` with `llmProvider: openrouter` | yours, pasted into the encrypted `llmApiKey` field |
| A PageRecord v1 reader Actor (optional) | `readerActorId` for JavaScript-heavy or blocked pages | LeadProof's **Web Page Reader** is public and priced per page read, billed to your account; any other reader Actor you can run also works. Bound it with `readerMaxTotalChargeUsd` |
| Nothing else | deterministic mode, records mode, dataset mode | no proxy, no API key, no external service |

### Limits and known gaps

- The built-in reader fetches static HTML only: no JavaScript rendering, no proxy, no login, no CAPTCHA.
  Sites that block datacenter traffic fail with an HTTP error. For those, pass `readerActorId` (a PageRecord v1
  reader) or provide page content as records.
- Deterministic mode needs the page to ship the markup it binds to. Many shops render prices in JavaScript or send
  no Product markup at all; those pages come back empty in deterministic mode. Use `auto` or `llm` for them.
- When a page marks up several products or businesses of the same type (a product page that also marks up related
  items), a single-valued field gets a `conflict` instead of a value: the Actor does not pick one for you. Bind a
  narrower path, use arrays, or use `auto`/`llm` mode.
- Publisher/subject separation relies on JSON-LD (publisher references, business entities) and page domains.
  On text-only pages a directory's footer contact can only be separated by the model's instructions.
- In `auto` mode, a field filled deterministically is not cross-checked against the page text (a JSON-LD
  brand can differ from a features table).
- Models sometimes skip evidence entries; those values are rejected, not kept. Expect lower coverage from
  weaker models.
- Interpretation checks are lexical (field words, negation words in 7 languages, other enum values). A quote in
  another language than the field name needs page-language words in the field's description.
- Bounds: `recordTimeoutSecs` per record, 60 s per model request with 2 transport retries and at most one repair,
  one retry for transient page-fetch failures, the run's own timeout. A record over its limit fails with
  `record_timeout`; the rest of the run continues.
- With `readerActorId`, the reader decides how pages are fetched (browser, proxy); this Actor only enforces
  robots.txt and URL safety before handing URLs over.
- Pages are sent to the model provider you choose. Do not use AI modes for content you may not share with it.

### Security

URLs must be public http(s) on standard ports without credentials. DNS is checked at connect time on every
request and every redirect, so hosts that resolve to private, loopback, link-local or cloud-metadata
addresses are refused (`unsafe_url`, `unsafe_destination`). Responses are size-, time- and redirect-bounded;
requests to one host are at least 1 second apart. Page content is data: nothing in it is executed, and no
link in it is followed. Secrets live only in encrypted input or Apify environment variables and are never
written to output or logs.

### Integration

Consumes PageRecord v1 (`contracts/page-record.v1.consumer.schema.json`, a snapshot of the reader's canonical
schema) and produces ExtractionResult v1 (`contracts/extraction-result.v1.schema.json`). Chain it after a page
reader by passing the reader's dataset as `datasetId`, or set `readerActorId` (tested live with
`leadproof/web-page-reader`: IDs, duplicates, failed pages, crash-and-resurrect without a second read).

### Other LeadProof Actors

- **Web Page Reader** - URLs to clean text, Markdown, links and JSON-LD (PageRecord v1), the input this Actor consumes.
- **Business Enrichment** - company profiles and contacts from a name or a website.
- **Website Email Finder**, **Bulk Email Verifier** - find and verify business email addresses.
- **Google Maps Verified Leads**, **Google Maps Scraper** - local business data.
- Lead lists built to order: [leadproof.co](https://leadproof.co).

### Fair use and legal

This Actor reads pages that are public, honours `robots.txt` by default, spaces requests to the same host, and does
not log in, solve CAPTCHAs or bypass blocks. You decide which URLs to send and you are responsible for the terms of
the sites you read and for how you use the data, including personal data (GDPR and similar laws). Page content is
treated as data: nothing in it is executed, and instructions hidden in a page cannot steer the model.

### FAQ

**Does evidence mean the data is correct?** No. Evidence shows what the page states and where. A page can be wrong,
stale, or about a different entity. Values that cannot be traced to the page are rejected rather than kept.

**Why is a field missing?** The page did not state it, the value had no valid evidence (see `rejected`), or the page
stated conflicting values (see `conflicts`). Missing facts are never filled with a guess.

**Do I need an AI key?** No. Deterministic mode uses no model. In `llm` and `auto` modes the run uses Apify's
OpenRouter Actor with your own Apify token, or your OpenRouter key if you prefer.

**Which model does it use?** `openai/gpt-4.1-mini` by default; any OpenRouter model with structured output and a
published token price works. The model, tokens and cost are reported on every row.

**Can it crawl a whole site?** No. It extracts from the pages you give it. Pair it with a crawler or a reader Actor.

### Support

Open an issue on the Actor's Issues tab with the run ID. Third-party licenses: `THIRD_PARTY_NOTICES.md`.

# Actor input Schema

## `sourceMode` (type: `string`):

Where the page content comes from. Only the selected source is used.

## `urls` (type: `array`):

Public http(s) pages, one request each (static HTML, no browser). robots.txt is respected by default. Private, local and cloud-metadata addresses are refused, including after redirects.

## `records` (type: `array`):

Objects with url (absolute, used as the evidence source) and at least one of text, markdown, html (up to 2 MB) or structuredData; optional inputId and title. PageRecord v1 objects are accepted as they are.

## `datasetId` (type: `string`):

Dataset of PageRecord v1 items, for example a page reader run's output. Declared as a dataset resource with READ permission, so a limited-permission run can read the dataset you select.

## `datasetOffset` (type: `integer`):

Skip this many dataset items first.

## `schemaPreset` (type: `string`):

Built-in schemas with deterministic bindings. Choose custom to use your own schema.

## `extractionSchema` (type: `object`):

JSON Schema subset: object root; types string, number, integer, boolean, object, array, and \[type, "null"]; keywords properties, required, items, enum, format (email, uri, date, date-time), minimum, maximum, minLength, maxLength, minItems, maxItems, title, description, x-extract. Refused: $ref, anyOf/oneOf/allOf, pattern, default, const, examples, open objects. Limits: 16 KB, depth 5, 80 properties, 60 fields. See README.

## `mode` (type: `string`):

Deterministic never calls a model. LLM and auto call the selected model, bounded by the LLM budget. Every AI value must quote the page or it is rejected.

## `llmProvider` (type: `string`):

The Apify proxy uses this run's own Apify token and bills token usage to the account that runs the Actor. With openrouter, your key is sent only to openrouter.ai.

## `llmModel` (type: `string`):

OpenRouter model ID with structured output support and a fixed token price. Checked against the provider catalog before any call.

## `llmApiKey` (type: `string`):

Only for provider openrouter. Stored encrypted by Apify.

## `maxLlmCostUsd` (type: `number`):

Each model call reserves its worst-case price first; calls that would exceed this budget are not sent and their records are skipped. This budget is separate from the Actor's maximum charge per run.

## `maxInputChars` (type: `integer`):

Longer pages are cut at a line boundary and flagged content\_truncated.

## `maxOutputTokens` (type: `integer`):

Output that hits this limit is reported as model\_output\_truncated, never repaired.

## `interpretedValues` (type: `string`):

A boolean such as inStock, or an enum value the page words differently, is the model's reading of a quote, not text found on the page. It is checked (the quote must be about the field and must not contradict the value) but never proven.

## `recordTimeoutSecs` (type: `integer`):

Reading plus extraction of one record, including model retries. A record over the limit fails with record\_timeout; the run continues.

## `maxRecords` (type: `integer`):

Hard limit for the selected source, API and Console alike.

## `concurrency` (type: `integer`):

Pages processed at once. Requests to one host are spaced at least 1 second apart.

## `requestTimeoutSecs` (type: `integer`):

Per page request, including redirects.

## `maxPageBytes` (type: `integer`):

Larger responses fail with response\_too\_large.

## `respectRobotsTxt` (type: `boolean`):

Skip URLs that the site's robots.txt disallows.

## `saveSourceRecords` (type: `boolean`):

Store each source PageRecord as PAGE-<inputId> in the key-value store, so every recordPath in the evidence can be audited. Adds storage use.

## `readerActorId` (type: `string`):

Read URLs through a PageRecord v1 reader Actor instead of the built-in fetcher. It runs as a separate run on your account.

## `readerUrlsField` (type: `string`):

Input field name the reader Actor expects for its URL list.

## `readerMaxTotalChargeUsd` (type: `number`):

Maximum this run lets the reader Actor charge. It is a separate run with its own price, so this run's maximum charge does not cover it. 0 means the reader may not charge at all.

## Actor input object example

```json
{
  "sourceMode": "urls",
  "urls": [
    "https://web-scraping.dev/product/1"
  ],
  "datasetOffset": 0,
  "schemaPreset": "product",
  "mode": "deterministic",
  "llmProvider": "apify-openrouter",
  "llmModel": "openai/gpt-4.1-mini",
  "maxLlmCostUsd": 0.25,
  "maxInputChars": 20000,
  "maxOutputTokens": 1500,
  "interpretedValues": "separate",
  "recordTimeoutSecs": 120,
  "maxRecords": 50,
  "concurrency": 4,
  "requestTimeoutSecs": 30,
  "maxPageBytes": 2000000,
  "respectRobotsTxt": true,
  "saveSourceRecords": false,
  "readerUrlsField": "urls",
  "readerMaxTotalChargeUsd": 1
}
```

# Actor output Schema

## `results` (type: `string`):

Full JSON: data, validation, evidence, conflicts, rejected values and transformations.

## `evidence` (type: `string`):

Evidence, conflicts and rejected values per record.

## `summary` (type: `string`):

Counts by status, stop reason, fetch statistics and LLM usage and cost.

## `schema` (type: `string`):

The validated schema used for this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "urls": [
        "https://web-scraping.dev/product/1"
    ],
    "schemaPreset": "product"
};

// Run the Actor and wait for it to finish
const run = await client.actor("leadproof/structured-web-extractor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "urls": ["https://web-scraping.dev/product/1"],
    "schemaPreset": "product",
}

# Run the Actor and wait for it to finish
run = client.actor("leadproof/structured-web-extractor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "urls": [
    "https://web-scraping.dev/product/1"
  ],
  "schemaPreset": "product"
}' |
apify call leadproof/structured-web-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,leadproof/structured-web-extractor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/oZ5YUTeMXooP6TW4A/builds/oUXyMithYO8PTafiT/openapi.json
