Tutorial8 min read

How to Scrape Hidden APIs and Embedded JSON

Short answer

To scrape JSON data in Python, open the page with your browser's DevTools, filter the Network panel to Fetch/XHR, find the request that returns the data, and call that URL with requests, using response.json() instead of parsing HTML; if there is no such request, look for JSON inside a <script> tag and load it with json.loads(). This skips the browser and the HTML parser, so it is faster, breaks less often and moves far fewer bytes. Through ProxyField residential proxies those requests cost from $1.95/GB; start with the Easy Trial: 1 GB for $2.95.

residential IPs
412M+
from, traffic never expires
$1.95/GB
Easy Trial
1 GB for $2.95
On this page 10 sections
  1. What a hidden API is
  2. Step 1: find the request in DevTools
  3. Step 2: copy the request as cURL
  4. Step 3: call the JSON endpoint from Python
  5. Step 4: JSON embedded in the HTML
  6. Step 5: pagination, tokens and POST requests
  7. When the page needs a browser
  8. Is scraping a hidden API allowed?
  9. Proxies for JSON scraping
  10. How to Scrape Hidden APIs and Embedded JSON FAQ

What a hidden API is

Most modern pages are a shell that JavaScript fills with data after it loads. That data comes from somewhere: a request to the site's own backend, usually returning JSON, or a block of JSON the server already put into the HTML. Neither is secret. These are the site's internal APIs, undocumented but visible to anyone who opens the browser's developer tools, and they are what "hidden API" means in scraping.

Scraping them has three advantages over parsing the rendered page:

  • Clean data. You get fields with names (price, stock, author) instead of text you must cut out of HTML.
  • Stability. A redesign changes the HTML far more often than the JSON behind it.
  • Less traffic and no browser. One JSON response replaces a page plus its scripts, styles and images, and a plain HTTP client replaces a headless browser.

Scrapy's documentation recommends the same order: find the data source first, use the browser's network tool to find the request, and reproduce it; a headless browser is the last resort (Scrapy: selecting dynamically-loaded content).

Step 1: find the request in DevTools

  1. Open the page in Chrome or Edge, press F12, and open the Network panel.
  2. Click the Fetch/XHR filter, so only data requests are listed (Chrome DevTools: Network reference).
  3. Reload the page, scroll, click "next" or "load more": whatever makes new data appear.
  4. Click each new request and open Preview or Response. You are looking for JSON that contains the values you see on the page. Searching the panel (Ctrl+F) for a product name or price finds it fast.
  5. Note the request's URL, method (GET or POST), query parameters, and the headers on the Headers tab.

On the practice page quotes.toscrape.com/scroll, scrolling down fires GET /api/quotes?page=2, then page=3, and so on. Each response is JSON with a quotes list and a has_next flag.

Step 2: copy the request as cURL

Right-click the request, choose Copy, then Copy as cURL. DevTools copies the full request, with headers and cookies, as a command you can paste into a terminal to check that it works outside the browser. Then remove headers one by one until you know which ones the server needs; usually that is a few of Accept, Referer, User-Agent, a cookie, or a token header.

Two tools turn the cURL command into code:

  • Scrapy: scrapy.Request.from_curl("curl 'https://...' -H '...'") builds the same request in a spider. We checked it on October 23, 2026 with Scrapy 2.19.0; it kept the URL, method and headers.
  • Python Requests: copy the URL, params and the headers you kept into a requests.Session, as below.

More on cURL itself: cURL with proxies.

Step 3: call the JSON endpoint from Python

Python
# pip install requests
import time
import requests

session = requests.Session()
session.headers.update({
    "Accept": "application/json",
    "Referer": "https://quotes.toscrape.com/scroll",
    "User-Agent": "Mozilla/5.0 (compatible; my-scraper/1.0)",
})

quotes, page = [], 1
while True:
    r = session.get("https://quotes.toscrape.com/api/quotes", params={"page": page}, timeout=30)
    r.raise_for_status()
    data = r.json()                      # already a Python dict: no HTML parsing
    for q in data["quotes"]:
        quotes.append({"text": q["text"], "author": q["author"]["name"], "tags": q["tags"]})
    if not data["has_next"]:
        break
    page += 1
    time.sleep(0.5)

print(len(quotes), quotes[0]["author"])

We ran it on October 23, 2026 (Requests 2.34.2): 10 requests, 100 quotes, no HTML parsed. r.json() returns Python dicts and lists, so the fields are ready to write to CSV, a database or a pandas DataFrame (pandas.json_normalize(quotes) flattens nested fields).

Step 4: JSON embedded in the HTML

Sometimes there is no data request, because the server puts the data into the HTML for the page's JavaScript to read. View the page source (not the Elements panel) and search for a value you see on the page. Common places:

  • A JavaScript variable such as var data = [...] or window.__INITIAL_STATE__ = {...}.
  • A framework's data tag, such as <script id="__NEXT_DATA__" type="application/json"> on sites built with Next.js's Pages Router, or similar state blocks in other frameworks.
  • JSON-LD in <script type="application/ld+json">: structured data (product, price, rating, article) that sites add for search engines; see HTML scraping for an example.

The JavaScript version of Quotes to Scrape keeps its quotes in a variable:

Python
import json, re
import requests

html = requests.get("https://quotes.toscrape.com/js/", timeout=30).text
match = re.search(r"var data = (\[.*?\]);", html, re.S)
data = json.loads(match.group(1))
print(len(data), data[0]["author"]["name"], data[0]["tags"])

It printed 10 quotes when we ran it. A regular expression is fine when the JSON sits in a clear var x = ...; statement, as Scrapy's docs also show. When the object is JavaScript rather than strict JSON (unquoted keys, trailing commas), json.loads() fails; the chompjs package parses JavaScript objects into Python dicts. For a <script type="application/json"> tag, select it with Beautiful Soup or parsel and pass its text straight to json.loads().

Step 5: pagination, tokens and POST requests

Real endpoints add details the practice site does not:

What you seeWhat to do
page=2, offset=40, start=40Increase the number until the response is empty or a has_next/total field says stop
A cursor, next or after value in the responseSend it back in the next request; you cannot jump ahead
A POST with a JSON bodySend the same body with session.post(url, json=payload)
GraphQL (/graphql, a query and variables in the body)Copy the query and change only the variables, such as the ID or the cursor
A token header (x-csrf-token, authorization) taken from the pageLoad the page once, read the token from the HTML or a cookie, and reuse it in the session
A signature or token you cannot reproduceFall back to a browser: let Playwright load the page and read the response (page.on("response"))

Keep the request rate close to what a person browsing would cause, read Retry-After on 429 responses, and stop if the endpoint starts to fail. Web scraping best practices covers retries and backoff.

When the page needs a browser

Some sites build requests with signatures computed in JavaScript, or only return data after a browser check. Then let a browser do the requesting and capture the JSON as it arrives. In Playwright for Python, page.on("response", handler) hands you each response, and response.json() gives the data without touching the DOM. You still avoid parsing HTML; you only borrow the browser to make the call. More on this in Python dynamic web scraping and how to scrape dynamic web pages.

Is scraping a hidden API allowed?

An internal API returns the same public data as the page, so the same rules apply as for scraping the page: collect public data, respect the site's terms and robots.txt, avoid personal data without a lawful basis, and keep the load low. Do not use credentials, keys or sessions that are not yours, and do not get around a login or a paywall. Official, documented APIs are the better route when the site offers one; web scraping vs API compares the two, and is web scraping legal covers the law.

Proxies for JSON scraping

JSON endpoints can be rate-limited per IP like pages. Spread requests over rotating IPs and keep a sticky IP when a token or cookie is tied to the session. In Requests, set the proxy once on the session:

Python
session.proxies = {
    "http": "http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000",
    "https": "http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000",
}

More options, including sessions and SOCKS5, are in Python Requests proxy setup. ProxyField gives every request a new IP, or a sticky session from 30 s to 36 h, over HTTP, HTTPS or SOCKS5, with targeting by country, region (state) and city. Because JSON responses are small, the traffic stays low: residential proxies cost $2.95/GB for 1 GB and fall to $1.95/GB from 1,000 GB, with no subscription; traffic never expires, and all purchases are final. Start with residential proxies and the Easy Trial: 1 GB for $2.95.

How to Scrape Hidden APIs and Embedded JSON FAQ

How do I scrape JSON data in Python?

ProxyField residential proxies carry these small requests from $1.95/GB; to scrape JSON data in Python, find the request that returns JSON in the browser's Network panel (Fetch/XHR filter), copy its URL, parameters and needed headers, and call it with requests, reading the result with response.json(). If the data is inside the HTML instead, extract the <script> content and load it with json.loads().

Why scrape hidden APIs?

Because the JSON behind a page is cleaner, more stable and much smaller than the rendered page. You get named fields instead of text cut out of HTML, a redesign rarely breaks it, and a plain HTTP client replaces a headless browser.

What is XHR and why is it important for scraping?

XHR (XMLHttpRequest) and its modern successor, fetch, are how a page's JavaScript requests data from the server after the page loads. In DevTools these requests sit under the Fetch/XHR filter, and they usually carry exactly the data you want to scrape, as JSON.

How do I handle JavaScript when scraping JSON data?

You usually do not need to run it: JavaScript only fetches the JSON or reads it from the HTML, and you can do both directly. If the request needs a signature that JavaScript computes, load the page in Playwright and capture the JSON responses with page.on("response").

Will I get blocked when scraping JSON data?

You can be, because sites can rate-limit their internal APIs per IP, as they do pages. Keep a modest pace, honor Retry-After on 429 responses, send the headers the browser sends, and spread volume over rotating residential IPs.

How to pass JSON in requests in Python?

Use the json= argument: requests.post(url, json={"query": "shoes", "page": 2}) encodes the dict and sets the Content-Type: application/json header for you. To read a JSON response, call response.json().

How do I parse nested JSON in Python?

response.json() gives you dicts and lists, so you walk them with keys and indexes, for example data["quotes"][0]["author"]["name"]. Use .get() for fields that may be missing, and pandas.json_normalize() to flatten nested records into table columns.

How do I find API endpoints on a website?

Open DevTools, select the Network panel and the Fetch/XHR filter, reload and interact with the page, then search the requests for a value you can see on screen. The matching request's URL is the endpoint; its Headers tab shows the method, parameters and headers to copy.

Can I scrape data from an API with Python?

Yes. Requests or httpx call the endpoint, response.json() turns the answer into Python objects, and a loop over the page, offset or cursor parameter collects every page. Save the records with the csv module, a database driver or pandas.

Show 2 more questionsShow fewer questions

Can I use Postman for web scraping?

Postman helps you explore an endpoint: paste the request you copied as cURL, change parameters and headers, and see what the server needs. It can also generate Python code for the request, but the scraper that loops over pages is still a script.

How do I scrape a GraphQL API?

Copy the POST request to the /graphql endpoint from DevTools, keep its query text as it is, and change only the variables, such as an ID or a pagination cursor. Send it with session.post(url, json={"query": query, "variables": variables}) and read the result from response.json()["data"].

Easy Trial: 1 GB for $2.95.

Try for the price of 1 GB, no subscription, traffic never expires.