Structured Web Extractor - JSON Schema with Evidence avatar

Structured Web Extractor - JSON Schema with Evidence

Pricing

from $2.00 / 1,000 extracted records

Go to Apify Store
Structured Web Extractor - JSON Schema with Evidence

Structured Web Extractor - JSON Schema with Evidence

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.

Pricing

from $2.00 / 1,000 extracted records

Rating

0.0

(0)

Developer

Lead Proof

Lead Proof

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

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

ExampleWhat it shows
Business contact details from company websiteslocal-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 markupA 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

FieldMeaning
sourceModeurls (default), records or dataset. Only the selected source is used.
urlsPublic http(s) pages. One static-HTML request each (no browser, no proxy).
recordsObjects 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, datasetOffsetDataset 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 inputIds.
schemaPresetproduct, local-business or custom.
extractionSchemaYour schema (see below). Used when the preset is custom.
modedeterministic, llm or auto.
llmProviderapify-openrouter (default; billed to the Apify account running the Actor) or openrouter (your own key).
llmModelOpenRouter model ID with structured output and a fixed price. Default openai/gpt-4.1-mini.
llmApiKeySecret field, stored encrypted by Apify. Only for openrouter.
maxLlmCostUsdLLM budget per run, default 0.25 USD (0.01-20).
maxInputChars, maxOutputTokensModel input and output bounds (default 20,000 characters and 1,500 tokens).
interpretedValuesseparate (default): interpretations are listed in interpretations, not in data. include: also in data, still listed.
recordTimeoutSecsTime limit per record, default 120 s (20-600), covering reading, extraction and model retries.
maxRecords, concurrency, requestTimeoutSecs, maxPageBytesHard limits, enforced for API input too.
respectRobotsTxtDefault true (RFC 9309, wildcards included).
saveSourceRecordsStore each source PageRecord as PAGE-<inputId> so every recordPath can be audited.
readerMaxTotalChargeUsdMaximum the reader Actor may charge in its own run, default $1. Separate from this run's maximum charge.
readerActorId, readerUrlsFieldRead 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):

{"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):

BindingReads
jsonld:Product.offers.priceJSON-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.nameSeveral types.
jsonld:@.priceRelative 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:phoneAddresses 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):

{
"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):

"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.

FieldMeaning
statussucceeded: 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).
dataYour schema's fields.
evidenceJSON 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).
interpretationsValues that are the model's reading of a quote (booleans, enum values the page words differently), with their evidence and inData.
rejectedValues 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...
conflictsFields where the page states different values, each with evidence.
transformationsEvery 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.
modelProvider, endpoint, model, attempts, repairs, tokens and provider-reported cost; null when no model was called.
timingsSeconds 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)ModelPage reader
Deterministic, built-in reader (default)100 x $0.002 = $0.20, plus $0.00005 to startnonenone
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.20none$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.

CostYou start this Actor yourselfAnother Actor starts it inside its own runBasis
Extraction: this Actor's events ($0.002 per extracted record)youthe account running the calling Actorverified 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 storagethe Actor owner under pay-per-eventthe Actor owner under pay-per-eventdocumented (Apify's pay-per-event profit formula)
Model tokens, apify-openrouter (llm and auto modes)you, billed by Apify's OpenRouter Actorthe account running the calling Actorverified 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 provideryour own OpenRouter keythe key supplied in that run's inputverified by design: the key is sent only to openrouter.ai
Page reader Actor (readerActorId)you, at that Actor's pricethe account running the calling Actorverified 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 startedsameverified 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

DependencyNeeded forAccess
An Apify accountevery runyours
apify/openrouter (public Actor)llm and auto modes; the run's own token authenticates itpublic on Apify Store, billed to your account, no signup or key
Your own OpenRouter key (optional)llm/auto with llmProvider: openrouteryours, pasted into the encrypted llmApiKey field
A PageRecord v1 reader Actor (optional)readerActorId for JavaScript-heavy or blocked pagesLeadProof'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 elsedeterministic mode, records mode, dataset modeno 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.

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.