WebGraph

POST /api/extract

Facts under a JSON Schema, each with its provenance, read from the page's own structured data and never guessed.

POST /api/extract maps a page's structured data - JSON-LD, microdata, Open Graph, __NEXT_DATA__, RSC flight data, Nuxt and __INITIAL_STATE__ payloads - onto a JSON Schema and returns one fact per matched path. Every fact says where it came from and how confident the engine is; a fact without a source cannot be checked by whoever consumes it.

Request

{
  "url": "https://shop.example.com/p/desk-lamp",
  "schema": {
    "type": "object",
    "properties": {
      "name": { "type": "string" },
      "offers": {
        "type": "object",
        "properties": {
          "price": { "type": "number" },
          "priceCurrency": { "type": "string" }
        }
      }
    }
  },
  "render": false,
  "rtl": false
}
FieldTypeDefaultMeaning
urlstringrequiredPage to extract from; http:// or https://
schemaobjectnullA JSON Schema object with properties. Omit it and the engine classifies the page and uses the schema for that type
renderbooleanfalseForce a browser render; needed for client-rendered pages whose payload is only present after hydration
rtlbooleanfalseReading direction for block ordering; false forces left-to-right, true right-to-left. Never detected on this route

ExtractRequest has no html, fetch, renderOptions or include_hidden_text: this route always fetches, with the engine's defaults. A schema that is not an object or lacks properties is 422 with schema must be a JSON Schema object with 'properties'.

Matching

Keys are matched cheapest and most certain first: exact key, then an alias you declared with the x-webgraph-aliases schema extension, then a normalised key (priceAmount, price_amount and Price Amount all match price_amount), then a known schema.org alias, then a bounded descent into nested objects, nearest match first. Confidence falls with each step because each is a weaker claim about what the page meant.

Response

{
  "page": { "url": "https://shop.example.com/p/desk-lamp", "reading_order": "dom-fallback", "reading_order_measured": false, "blocks": 212, "frameworks": ["next"], "requires_render": false, "payloads": ["json-ld", "open-graph", "next-data"], "content_hash": "…", "dom_order_differs": false },
  "facts": {
    "name": { "value": "Arc desk lamp", "confidence": 0.95, "extractor": "structured-data", "modality": "dom-json", "source": "json-ld:exact", "source_xpath": "/html/head/script[3]" },
    "offers.price": { "value": 89.0, "confidence": 0.9215, "extractor": "structured-data", "modality": "dom-json", "source": "json-ld:nested", "source_xpath": "/html/head/script[3]" },
    "offers.priceCurrency": { "value": "EUR", "confidence": 0.9215, "extractor": "structured-data", "modality": "dom-json", "source": "json-ld:nested", "source_xpath": "/html/head/script[3]" }
  },
  "schema_choice": null
}

page is the same PageInfo the text route returns. facts is keyed by dotted schema path, sorted; array items appear as plans.0.price.

FactOut

FieldTypeMeaning
valueanyThe coerced value, typed as the schema asked
confidencenumberbase(match) × weight(source) × 0.97^depth, capped at 1.0. Bases: exact 0.95, declared alias 0.92, normalised 0.88, known alias 0.80, nested 0.72. Source weights: JSON-LD 1.0, microdata 0.98, __NEXT_DATA__ 0.95, RSC/Nuxt 0.93, initial state 0.90, Open Graph 0.85
extractorstringWhich mechanism produced it. Today always structured-data; llm is declared for a path not yet built
modalitystringHow the content reached the engine. Every fact today is dom-json, lifted verbatim from a payload; the enum also defines text for values read from DOM text
sourcestring | null<payload source>:<how>, e.g. json-ld:exact, next-data:nested. Auto-schema gap fills append (page wrapper) or (social preview)
source_xpathstring | nullXPath of the element that carried the payload

No schema: the engine chooses

With schema omitted the router types the page and the engine uses the built-in schema for that type - article, product, forum, service, collection/listing, documentation. It then reads only the structured-data node that describes the page: not the site's Organization, not its breadcrumbs. Without that gate a category page reports the shop's name as its own product 46% of the time. Fields no node about the page supplied are filled, at a discount, from the page's WebPage wrapper and then from its Open Graph tags, and each such fact's source says so.

schema_choice explains the choice:

FieldTypeMeaning
page_typestringThe type the router chose
confidencenumberIts probability
fieldsstring[]Properties of the schema that was used
subject_typesstring[]The @type of each node accepted as describing this page. Empty means the page shipped data about its site or breadcrumbs but nothing about itself - the common case on category pages
payloads_considered, payloads_usedintegerPayloads seen and payloads read
filled_from_fallbackstring[]Paths taken from the wrapper or social preview because no node about the page had them

The gate runs only on an auto-chosen schema. A caller who wrote a schema may be reaching for the Organization on purpose.

Refusing to guess

Nothing here infers semantics. If no key matches a path, no fact is emitted; a missing value is recoverable downstream and a wrong one silently poisons everything built on it. Likewise, a page the router types as unknown gets an empty schema and no facts rather than a vocabulary picked at random, and schema_choice.fields is then []. "No price" therefore means different things depending on schema_choice.page_type, which is why it is returned.

JavaScript shells

A page whose static HTML is a shell - markup with no readable text until a script runs - is a refusal for /api/text, which would have nothing to say. It is a document for this route: the hydration payload (__NEXT_DATA__, a flight stream, JSON-LD) is complete in the shell and is the whole point of the page. With render: false the route tries a browser once if Playwright is installed; if the page stays empty, the facts are read from the shell's payload and page.blocks may be 0.

curl -s http://127.0.0.1:8000/api/extract \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://shop.example.com/p/desk-lamp",
       "schema": {"type": "object", "properties": {"name": {"type": "string"}}}}'

Status codes are those of /api/text: 422 for a bad URL or schema, 502 when the page could not be obtained or was a wall. See errors.