Skip to content
Structured Extraction2 credits

extract_embedded_state

Modern sites serialize the data their UI renders straight into the document. This reads it back: one fetch, exact values, and no model anywhere in the extraction path.

Use Cases

Exact prices from JavaScript-rendered pages

Prices, stock levels and IDs come from the site's own state rather than from a model reading rendered text, so there is nothing to fabricate.

Listings a plain scrape cannot see

Search results and product grids that render client-side are usually already present in __NEXT_DATA__ or an RSC payload on the first response.

Cheaper than an LLM extraction

2 credits against 3 for extract_with_llm or extract_structured, with no inference step to wait for.

Auditing what a site publishes about itself

found reports every state source on the page and its size, which is often more than the visible UI shows.

Endpoint

POST/api/v1/tools/extract_embedded_state
Auth Required
1 req/s on Free plan
2 credits

Parameters

NameTypeRequiredDefaultDescription
url
stringRequired-
Page to read embedded state from.
Example: https://example.com/products/widget
path
stringOptional-
Return one subtree instead of the whole payload. Dotted keys and array indexes only - this is not JSONPath, so there are no wildcards, filters, slices or recursive descent. A path that does not resolve comes back as a 400 naming the keys that were available where it stopped, and costs no credits.
Example: next_data.props.pageProps
user_agent
stringOptional-
Override the User-Agent sent to the target. The request stays signed as CrawlForge; the signature covers the authority, not this header.
Example: MyCompanyBot/1.0
respect_robots
booleanOptionaltrue
Respect the target site's robots.txt. Left at `true`, a path disallowed for `CrawlForge` is refused with 403 before anything is fetched and no credits are charged. Set it to `false` only for a target you have your own agreement with - the response then carries a `warnings` entry and the override is recorded against your API key.
Example: true
timeout
numberOptional20000
Fetch timeout in milliseconds, between 1000 and 60000. State payloads are often megabytes, so the default is higher than on the lighter extraction tools.
Example: 20000
keys_only
booleanOptionalfalse
Return `keys` instead of `data`: the shape of the selected data (after `path`, if given) two levels deep, each leaf labelled `object`, `array(<n>)`, `string`, `number`, `boolean` or `null`, and a selected array shown as `{ length, first }` with the shape of its first element. `found` and `bytes` are unchanged. One cheap call shows which `path` to ask for before you pull a multi-megabyte payload; it costs the same 2 credits.
Example: true
find
stringOptional-
Return `matches` instead of `data`: every property with this key name (case-insensitive) anywhere in the selected data (after `path`), in document order, each as {path, preview} with the first 200 characters of its value - at most 50, with `matches_total` and `matches_truncated`. Each match path already starts with `path`, so it can be passed straight back as `path`. Discover where a field lives without downloading the payload. Cannot be combined with keys_only
Example: specialty
raw
booleanOptionalfalse
Also keep the undecoded __NUXT_DATA__ devalue array under json_scripts, beside the decoded nuxt_data. Default: false
Example: true
escalate
booleanOptionalfalse
Opt in to one automatic retry through the stealth browser when the plain fetch meets a wall instead of the page — a Cloudflare, Amazon, DataDome, PerimeterX, Akamai or Vercel challenge page, a bare 403, 429 or 444, or an empty shell that carries no state. The plain fetch always runs first; a 404 or a 5xx never escalates. The rendered HTML goes through the same parser, so the response has the same shape, plus `escalated` (whenever this is set) and `stealth: { engine, vendor_detected }` when the render ran. Escalation reuses the same stealth path as [stealth_mode](/docs/api-reference/tools/stealth-mode) and the same robots.txt gate, and adds no new handling of a bot defence; a wall the render is refused at too still comes back as a blocked page, charged nothing. Adds 5 credits, charged only when the render actually runs. Reading `window` globals (`window_state`) is available on the MCP server only: this API's render returns HTML.
Example: true
escalate_engine
stringOptionalplaywright
Browser engine for the escalation stage: "playwright" (default) or "camoufox" (Firefox-based, stronger anti-detection; available only where installed on the backend). Ignored unless `escalate` is `true` and the plain fetch was walled.
Example: camoufox
wait_for
numberOptional-
Extra wait after page load in the escalated render, in milliseconds (0-30000), for state a page writes after DOMContentLoaded. Passed to the stealth render; ignored unless `escalate` is `true` and the plain fetch was walled.
Example: 2000
max_inline_chars
numberOptional40000
Inline size threshold in characters of the result's JSON (1,000-10,000,000). Over it, the full result is stored for 1 hour and the response keeps `url`, `path`, `bytes`, `found` and `data_keys` (the top-level keys of `data`, or `array(<n>)`), plus `preview` — the head of the pretty-printed JSON, cut at a line boundary so no value is ever split — `result_handle`, `total_chars` and `truncated: true`. The whole response stays within this size. Read any field of the stored copy with [read_result](/docs/api-reference/tools/read-result) `json_path`, e.g. `data.next_data.props`, for 1 credit per call.
Example: 40000

What it reads

Each source the page carries is listed in found and keyed in data by its name. A global that is not plain JSON is read as a JavaScript literal evaluated statically - objects, arrays, strings, numbers and Nuxt 2's single-return function - and no page code ever runs; anything that calls code or reads other variables is named in warnings instead.

  • next_data - Next.js __NEXT_DATA__.
  • next_f - React Server Component flight rows (self.__next_f), keyed by row id, with $<id> references between rows resolved in place (at most doubling the payload).
  • data_rows - an index of the next_f rows that carry data rather than markup, largest first, each { id, bytes, keys, length }. Product and listing data usually lives in these rows; read one with path: "next_f.<id>".
  • nuxt - Nuxt 2 __NUXT__; nuxt_data - Nuxt 3 __NUXT_DATA__, decoded from its index array into plain objects.
  • sveltekit_data - SvelteKit's kit.start data, one entry per route node, root layout first.
  • yt_initial_data and yt_initial_player_response - YouTube's ytInitialData and ytInitialPlayerResponse.
  • apollo_state, apollo_ssr_transport, initial_state, preloaded_state, preloaded_data, remix_context (__remixContext), tgt_data (__TGT_DATA__), pws_data (__PWS_DATA__), server_data (__SERVER_DATA__), app_state (__APP_STATE__), state (__STATE__) and global_data (__data__) - Apollo, Redux-style and other window globals.
  • inertia_page - Inertia's data-page JSON.
  • shopify - ShopifyAnalytics.meta and Shopify's product JSON scripts. Meta prices are integers in minor units: 10500 is 105.00 in meta.currency.
  • json_scripts - <script type="application/json"> and <script type="text/json"> blocks, each { id, data }.
  • json_attributes - data-* attributes holding a JSON object of 2 KB or more, each { tag, attribute, id, data }.

Find a path

On a multi-megabyte payload, ask for the key you want instead of the data. find searches the whole payload, or only inside path when you send one, and returns where the key occurs with the head of each value.

terminalBash
curl -X POST https://crawlforge.dev/api/v1/tools/extract_embedded_state \
  -H "X-API-Key: cf_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "path": "yt_initial_player_response",
    "find": "viewCount"
  }'

Find response

matches replaces data. Each path already starts with the path you sent, so pass it straight back as path in the next call to get the full value. matches_total counts every match; matches_truncated is true when more than the 50 returned were found. A match marked addressable: false sits under a key containing . or [, which a path cannot spell.

response.jsonJson
{
  "success": true,
  "data": {
    "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "found": [
      { "name": "yt_initial_data", "variable": "ytInitialData", "bytes": 21239 },
      { "name": "yt_initial_player_response", "variable": "ytInitialPlayerResponse", "bytes": 8076 }
    ],
    "path": "yt_initial_player_response",
    "bytes": 8076,
    "matches": [
      {
        "path": "yt_initial_player_response.videoDetails.viewCount",
        "preview": "\"1822707973\""
      },
      {
        "path": "yt_initial_player_response.microformat.playerMicroformatRenderer.viewCount",
        "preview": "\"1822707973\""
      }
    ],
    "matches_total": 2,
    "matches_truncated": false,
    "warnings": []
  },
  "credits_used": 2,
  "credits_remaining": 996,
  "processing_time": 640
}

Request Examples

cURL - Read the whole state

terminalBash
curl -X POST https://crawlforge.dev/api/v1/tools/extract_embedded_state \
  -H "X-API-Key: cf_test_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/products/widget"
  }'

TypeScript - Scope it with a path

extractEmbeddedState.tsTypescript
// npm install crawlforge-sdk
import { CrawlForge, ValidationError } from 'crawlforge-sdk';

const client = new CrawlForge({ apiKey: process.env.CRAWLFORGE_API_KEY });

try {
  const result = await client.extractEmbeddedState({
    url: 'https://example.com/products/widget',
    // Dotted keys and array indexes. Not JSONPath: no wildcards or filters.
    path: 'next_data.props.pageProps',
  });

  // result.data is untyped in crawlforge-sdk 0.1 — its shape is the Response Example below.
  const { found, data } = result.data as {
    found: { name: string; variable: string; bytes: number }[];
    data: { product: { price: number } };
  };

  // Every state source on the page, largest first, whether or not it was scoped.
  for (const source of found) {
    console.log(source.name, source.variable, source.bytes);
  }

  // The values are the site's own, so they can be used as they are.
  console.log(data.product.price);
} catch (err) {
  // A path that does not resolve is a 400 naming the keys that were available.
  if (err instanceof ValidationError) {
    console.error(err.message);
  } else {
    throw err;
  }
}

Python - Discover, then scope

extract_embedded_state.pyPython
# pip install crawlforge
from crawlforge import CrawlForge

client = CrawlForge()  # reads CRAWLFORGE_API_KEY

# 1. Discover what the page carries. Nothing is truncated, so this can be big.
discovery = client.extract_embedded_state(
    url='https://example.com/products/widget',
)

# discovery.data is a plain dict — its shape is the Response Example below.
for source in discovery.data['found']:
    print(source['name'], source['variable'], source['bytes'])

# This tool's warnings live inside data, next to the state itself.
for warning in discovery.data['warnings']:
    print('warning:', warning)

# 2. Ask again for just the branch you want.
scoped = client.extract_embedded_state(
    url='https://example.com/products/widget',
    path='next_data.props.pageProps.product',
)

print(scoped.data['bytes'], 'bytes')
print(scoped.data['data'])

Response Example

200 OK980ms
{
"success": true,
"data": {
"url": "https://example.com/products/widget",
"found": [
{
"name": "next_data",
"variable": "__NEXT_DATA__",
"bytes": 125
}
],
"path": null,
"bytes": 139,
"data": {
"next_data": {
"buildId": "KfC_3GF1zuM",
"props": {
"pageProps": {
"product": {
"sku": "WID-9001",
"price": 149.99,
"currency": "USD",
"inStock": true
}
}
}
}
},
"warnings": []
},
"credits_used": 2,
"credits_remaining": 998,
"processing_time": 980
}
Field Descriptions
data.foundEvery state source on the page, with the raw thing it was read from and its serialized size
data.pathThe path that was applied, or null when the whole payload was returned
data.bytesSerialized size of what is being returned - after scoping, when a path was given
data.dataThe state itself, keyed by source name
data.warningsSources that were present but not parseable as JSON, and the size hint that suggests a path

Error Handling

Invalid parameters (400 Bad Request)

VALIDATION_ERROR, with the problem in details: for example find sent together with keys_only - each replaces data with its own answer, so send one per call - or a find that is empty or over 100 characters. Nothing is fetched and no credits are charged.

Path did not resolve (400 Bad Request)

The path does not exist in the extracted state. The message names the point it stopped at and the keys that were available there, so a typo comes back fixable. No credits are charged. Run the call once without a path to see what the page actually carries.

Blocked by robots.txt (403 Forbidden)

The target site's robots.txt disallows this path for CrawlForge. Set respect_robots: false to override if you have your own agreement with the target - the override is recorded against your API key. The override does not reach a host on CrawlForge's permanent opt-out list, which is refused whatever respect_robots is set to.

Response too large (413 Payload Too Large)

The page's HTML exceeds the 25MB body limit. The limit is on the page as served, not on the state extracted from it. Nothing is charged.

Target did not respond (504 Gateway Timeout)

The site did not answer within timeout. State-heavy pages are large; raise timeout toward its 60000 ceiling before treating this as a failure. Nothing is charged.

Blocked page (502 Bad Gateway)

The plain fetch met a bot-defence challenge page (BLOCKED, with the vendor in blocked) or an empty shell carrying no state (UNUSABLE_DOCUMENT). Nothing is charged, and the message names the next step: re-run with escalate: true. With escalate: true already set, the same error carries escalated: true — the stealth render was refused too — and nothing is charged.

Escalation backend not configured (503 TOOL_NOT_AVAILABLE)

escalate: true runs its stealth stage on the CrawlForge execution backend. When that backend is not configured the stage returns 503 and no credits are charged.

Related Tools