MegalaniaBlog
  1. Blog
  2. How to scrape
  3. Infinite scroll

How to scrape infinite scroll and load-more pages

Infinite scroll loads items as you scroll. First find the JSON it calls. If you cannot, scroll in a real browser with page actions. Code and costs inside.

Jordan HayesDeveloper Relations
6 min read
How to scrape
A tall page card with a scroll bar. Below the fold, new item cards slide up out of a dashed box, and a small arrow points down.

The short answer: an infinite-scroll page gets its next items from a JSON URL each time you reach the bottom. Call that URL yourself, page by page: it is free and fast. If you cannot, for example because the URL needs tokens the page makes on the fly, send the page to us with actions: we open it in a real Chrome, scroll to the bottom as many times as you ask, and return the full page. Add "network": "true" to also get the JSON the page loaded while it scrolled. Actions add 3 credits to a browser page, which starts at 15.

How infinite scroll works

The first HTML has 10 or 20 items. A script watches the scroll position. Near the bottom it calls a URL like /api/items?page=2, gets JSON, and adds the items to the page. A “Load more” button does the same on a click. So the data you want arrives as JSON, one page at a time, and the HTML you fetched first never has more than the first page.

The free way: call the JSON yourself

Open the page in your browser, then developer tools, Network tab, filter on “Fetch/XHR”, and scroll. Each new batch of items shows up as a request. Click it and look at the URL and the answer. On our practice page it looks like this:

cURL
curl -s "https://quotes.toscrape.com/api/quotes?page=2" | head -c 300

Change the page number and loop until the answer says there is no next page. This is the best way when it works: no browser, no HTML, clean JSON.

It stops working when the URL needs a token or a signature the page makes in JavaScript, when the API refuses requests that do not come from the page, or when the site blocks your server’s IP.

Check a site first

Before a big run, ask about the host. It is free. Over MCP, your agent calls check_site:

MCP check_site
{"name": "check_site", "arguments": {"host": "quotes.toscrape.com"}}

From cURL, the same answer is two calls: /v1/tier for the price, /v1/stats/domain for how our routes have done there lately.

cURL
curl -s -H "Authorization: Bearer YOUR_KEY" \
  "https://get.megalania.io/v1/tier?host=quotes.toscrape.com"
curl -s -H "Authorization: Bearer YOUR_KEY" \
  "https://get.megalania.io/v1/stats/domain?host=quotes.toscrape.com&days=30"

capabilities.browser must list a tier: actions need the browser. Check a site first explains every field.

Scroll in a real browser (cURL)

actions is a list of steps joined by |. This one scrolls to the bottom three times and waits 1.5 seconds after each, so the page can load the next items:

cURL
curl -sS https://get.megalania.io/v1 \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -D headers.txt -o page.md \
  -d '{"url": "https://quotes.toscrape.com/scroll", "actions": "scroll:bottom|wait:1500|scroll:bottom|wait:1500|scroll:bottom|wait:1500", "response_types": "markdown", "max_credits": "128"}'

page.md now holds the items of four loads, not one. actions needs the browser, so you do not have to send engine. A browser page costs more than HTTP, so we gave max_credits room.

For a “Load more” button, click it and wait for an item that only exists after the click:

text
click:button.load-more|wait_for:.item:nth-of-type(21)|click:button.load-more|wait:1500
Get a free beta key76,000 credits for 30 days. Sign in with Google or GitHub.Apply for the beta

The steps you can send

At most 20 entries, and 30 seconds for all of them together:

Entry What it does
scroll:bottom scrolls to the bottom; scroll:top and scroll:800 (pixels) work too
wait:1500 waits 1 to 10,000 ms
wait_for:.item waits up to 10 s for a visible element that matches
click:.more clicks the first visible match
type:#q=shoes types text into the first visible match
press:Enter presses one key

If a step cannot run, for example no visible match in time, the answer is a free 422 action_failed, and the message names the step’s index.

Python: the items and the JSON behind them

"network": "true" returns, next to the page, each JSON answer the page loaded while it scrolled. network_url_globs keeps only the ones you want. The answer is then multipart/mixed: one part for the page, one for the network log.

scroll_and_capture.py
# pip install requests
# Run: MEG_API_KEY=YOUR_KEY python scroll_and_capture.py https://quotes.toscrape.com/scroll
import json
import os
import sys
import uuid
from email.parser import BytesParser
from email.policy import default

import requests

resp = requests.post(
    "https://get.megalania.io/v1",
    headers={
        "Authorization": f"Bearer {os.environ['MEG_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "url": sys.argv[1],
        "actions": "scroll:bottom|wait:1500|scroll:bottom|wait:1500|scroll:bottom|wait:1500",
        "response_types": "markdown",
        "network": "true",
        "network_url_globs": "*/api/*",
        "max_credits": "128",
    },
    timeout=120,
)
if resp.status_code != 200:
    err = resp.json()["error"]
    sys.exit(f"{resp.status_code} {err['code']}: {err['message']}")

msg = BytesParser(policy=default).parsebytes(b"Content-Type: " + resp.headers["Content-Type"].encode() + b"\r\n\r\n" + resp.content)
for part in msg.iter_parts():
    name = part.get_param("name", header="content-disposition")
    if name == "markdown":
        with open("page.md", "w", encoding="utf-8") as f:
            f.write(part.get_content())
    if name == "network":
        for line in part.get_content().splitlines():
            r = json.loads(line)
            print(r["status"], r["url"], r["bytes"], "bytes")
print("credits:", resp.headers["X-Credits-Charged"])

Each network line is one response: url, method, status, content_type, bytes, truncated and body. Once you see the JSON URL there, try the free way above with it.

Try it on our Lab. lab.megalania.io/scroll is our own test page: ten items come with the page, and each scroll to the bottom adds ten more. Send actions=scroll:bottom|wait_for:#item-20 and you get twenty.

What it costs

A page with actions is a browser page, plus 3 credits however many steps run. Network capture adds 0.

Route Credits a page Included data With actions
Browser, datacenter (c:dc) 15 1 MiB 18
Browser, residential (c:res) 32 64 KiB 35

Each started 128 KiB above the included data adds 1 on c:dc and 8 on c:res. A page that scrolled four times is bigger than one that did not, so the size counts more on c:res. Run check_site (free) to see the price for your site. A block we detect is free, and so is a step that cannot run.

Limits

  • 30 seconds for all actions, inside timeout (at most 90).
  • Many pages: a batch takes actions and network too, with the same prices and the same failure cap.
  • Iframes and shadow roots: selectors do not reach into them.
  • Endless feeds: pick a number of scrolls. A feed that never ends never finishes.
  • Terms: this is not legal advice. Read each site’s terms and robots.txt, and follow them.

Every action and every network field: Page actions and Network capture.