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
/api/v1/tools/extract_embedded_stateParameters
A call costs 2 credits. With escalate: true the projection is the ceiling at 7 credits; the escalation stage's 5 credits are charged only when the plain fetch was walled and the stage actually ran. keys_only and find cost the same 2 credits.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
url | string | Required | - | Page to read embedded state from. Example: https://example.com/products/widget |
path | string | Optional | - | 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 | string | Optional | - | 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 | boolean | Optional | true | 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 | number | Optional | 20000 | 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 | boolean | Optional | false | 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 | string | Optional | - | 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 | boolean | Optional | false | Also keep the undecoded __NUXT_DATA__ devalue array under json_scripts, beside the decoded nuxt_data. Default: false Example: true |
escalate | boolean | Optional | false | 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 | string | Optional | playwright | 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 | number | Optional | - | 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 | number | Optional | 40000 | 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 thenext_frows that carry data rather than markup, largest first, each{ id, bytes, keys, length }. Product and listing data usually lives in these rows; read one withpath: "next_f.<id>".nuxt- Nuxt 2__NUXT__;nuxt_data- Nuxt 3__NUXT_DATA__, decoded from its index array into plain objects.sveltekit_data- SvelteKit'skit.startdata, one entry per route node, root layout first.yt_initial_dataandyt_initial_player_response- YouTube'sytInitialDataandytInitialPlayerResponse.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__) andglobal_data(__data__) - Apollo, Redux-style and otherwindowglobals.inertia_page- Inertia'sdata-pageJSON.shopify-ShopifyAnalytics.metaand Shopify's product JSON scripts. Meta prices are integers in minor units: 10500 is 105.00 inmeta.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.
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.
{
"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
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
// 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
# 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
{ "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}data.foundEvery state source on the page, with the raw thing it was read from and its serialized sizedata.pathThe path that was applied, or null when the whole payload was returneddata.bytesSerialized size of what is being returned - after scoping, when a path was givendata.dataThe state itself, keyed by source namedata.warningsSources that were present but not parseable as JSON, and the size hint that suggests a pathError 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
Ready to read a page's own data? Sign up for free and get 1,000 credits.