Skip to main content
POST /scrape opens a page in a ready browser and returns it as cleaned HTML, Markdown, links or a screenshot. The browser uses a residential proxy in the region set in your dashboard and a desktop fingerprint. It passes challenge pages and solves captchas on its own, and keeps nothing between requests. Run your own browser instead if pages need to share a login, or you need a specific country or city, your own proxy or fingerprint, extensions or a persistent profile. A coding agent can set this up from the setup prompt.

Scrape a page

Set your API token and base URL first.
Most pages come back in a second or two, a challenge page adds 10 to 60 seconds. A request gets 120 seconds in total.

Recipes

Each recipe is a single request. The SDK examples reuse the client from Scrape a page: body fields go in as keyword arguments in Python or an options object in JavaScript, and the result has the fields of data from Read the response.

An article as Markdown, for an LLM

Markdown of the main content, without menus, footers and sidebars. Headings and links stay.

A product page: name and price

wait_for waits for the price. The request returns as soon as it renders, or fails with selector_timeout if it never does.
links are absolute and deduplicated. This example keeps only links to other sites.

A screenshot of a page

A screenshot is taken only when formats includes screenshot; data.screenshot then holds a Base64 PNG of the whole page. It also loads images, fonts and media, so the request usually costs about twice as much. Pass block_resources to skip what the shot can do without.

A search typed into a page

Actions run on the loaded page with human-like input: click the search box, type the query, press Enter, wait for results, scroll. You get the page as the actions left it.

A listing that loads on scroll

Wait after each scroll so the next batch can load. The result has everything loaded by the end.

A page behind Cloudflare

The challenge page is passed before the page is captured, with a captcha solve if it has one. challenge in the response says what was there and what it cost. Give the request a longer timeout for the wait. A challenge that passes by itself is free.

Pages behind a login, in one browser

POST /scrape keeps nothing between requests, so for pages behind a login start your own browser. POST /profiles/{internal_uuid}/scrape takes the same body. Here the first request logs in, the second reads a page as that user. The demo site accepts any name and password.

Request parameters

The same body works on POST /scrape and on POST /profiles/{internal_uuid}/scrape.

Examples per option

Which wait to use

  • wait_until sets when navigation is done. The default domcontentloaded returns once the HTML is parsed, before late images and scripts, so a page drawn by scripts can still be blank in a screenshot. load waits for those too. networkidle waits for the network to go quiet and times out on pages with constant background traffic like analytics or chat widgets. commit returns on the first response, enough for a status check.
  • wait_for waits for one element. Try it first: point it at the content you need, such as a listing, a price or an article, and the request returns as soon as it appears.
  • wait is a fixed pause. Use it only when nothing on the page signals it is ready, since it adds time to every request.
  • actions are for pages that show content only after input: a search, a cookie prompt, a “load more” button, a tab. A wait action between steps waits for the result of the previous one.
A request runs in this order:
  1. Load url until wait_until, within timeout.
  2. Pause for wait seconds, then wait for wait_for, within timeout.
  3. Run actions in order.
  4. Capture formats.
After navigation and after every step the page is checked: a challenge page is passed and a captcha is solved. If a wait ended with a solve, it runs again. If a wait_for element doesn’t show up, the page is checked, and after a solve the request waits for the element once more. Actions are never interrupted or repeated: a captcha that shows up during an action is handled by the check after it.

Actions

If an action fails, the request ends with action_failed, and data has the page as the browser left it, plus challenge. If a click leads to a challenge page, the check after the click passes it.

Challenge pages and captchas

Three things can get in the way. A challenge page is what an anti-bot service shows instead of the page until the browser passes its check, usually with status 403, 405, 429 or 503. Some pass by themselves, some have a captcha. A block page is a refusal no check gets past, for example a banned exit IP, and is reported right away. A page captcha sits on the page itself and guards a form or an action. With auto_captcha_solve: true (the default) every challenge page and captcha the checks find is solved, paid types included. An invisible captcha runs only when the site calls it, usually when a form is sent. Until then there is nothing to solve and the request doesn’t wait for it; challenge shows it with solved: false if the check sees it. After a click that sends the form, the check after the click solves it. A captcha that appears after the last step isn’t seen, so end with a wait, wait_for or solve step. For a form behind a captcha you can also drive the browser over CDP and call Captcha.solve yourself. Every solve is a captcha charge, whatever the outcome. A challenge page that passes by itself is free.
If your content isn’t behind a challenge page, auto_captcha_solve: false is faster: captchas on the page are left alone and not charged.
If a challenge page can’t be passed, or a block page comes up, POST /scrape retries once in another browser with a different exit IP. A connection error (reset, closed or timed out) is retried the same way; a certificate error is not. A domain that doesn’t exist looks like a reset connection from the browser, so it is retried too, and comes back as site_unreachable after the second attempt. If it’s still blocked, you get the page as is with the site’s status, and the request isn’t billed for browser time or traffic. An unsolved page captcha doesn’t hold the request: the page comes back right away with challenge.solved: false. With auto_captcha_solve: false nothing is solved or waited for. You get the page as is, challenge still says what was there, and a challenge or block page returned this way isn’t billed for browser time. To choose the moment yourself, keep false and add a solve action where you expect the captcha. It solves what’s on the page at that point, including a widget still loading, and does nothing if there’s nothing to solve. A challenge page the action can’t pass comes back as is, without a retry.

Read the response

data.status is the site’s HTTP status after redirects and solves, so a 200 from Surfsky can carry a 403 from the site. Each format you ask for adds a key: content for html, plus raw_html, markdown, links (absolute, deduplicated), screenshot (Base64) and cookies (pass them as cookies to the next request). Links and image addresses in content and markdown are absolute; raw_html is the document as the browser has it. If the site answered with an error and an empty body, you get that status and empty content. A PDF comes back with its content_type and no text: its text isn’t extracted. challenge is null if nothing got in the way, otherwise it describes the last obstacle:
kind is challenge_page, block_page or page_captcha. captcha is the captcha type as named in Captcha.solve, or null for a challenge page without one. solved is true if the request got past it, by a solve or on its own, and false if it was still there at the end. cost is the total captcha charge for the request as a string, "0" if nothing was charged. receipt is what the request cost: the browser time billed, the traffic of the page, the captcha solves, and their total in USD. session_uuid is the request’s entry in session receipts and in the ledger. A request that isn’t billed has zeros for time and traffic. After a retry the receipt is of the final attempt, with the captcha solves of both. total_cost here includes traffic and captchas; in session receipts it is browser time only, and traffic and captchas are ledger entries of their own. It is on error responses too, and missing only when the request never reached a browser. A request that couldn’t finish returns HTTP 400 with success: false and a code: Other errors: In the SDKs a failed request raises APIError with the same status_code and code. body has the full response, page included:

Billing

You pay for a request when its response carries the page. That includes an error of your own wait_for, wait or action on a loaded page, and a load your own timeout cut short. A response with no page is free: a page the site refused, an unreachable site, a blank page, and a request we could not finish in time. A CAPTCHA solve is charged whatever became of the page. Browser time is billed in 10-second steps at your plan’s browser-hour rate, from the start of the page’s work to its delivery: a 4-second scrape counts as 10 seconds, a 25-second one as 30. Traffic is the page’s own, billed per GB, and CAPTCHA solves are billed at their own price. The price is the same whichever browser serves the request. On a reserved plan the monthly fee covers the browser time; traffic and solves still come from the balance. The response’s receipt has the amounts, and the charges are in the ledger. Billing covers the plans and where to see usage. Per plan, at the rates on the pricing page. A light page is 200 KB in one step, a typical page is 12 seconds and 0.9 MB. A request over your browser slots gets 429 parallel_browsers_limit_reached.

When to run your own browser

Start a browser, scrape through it, then stop it. POST /profiles/{internal_uuid}/scrape takes the same body, one page per request, and the pages share the browser’s cookies. The login recipe shows the calls in curl and the SDKs. In the SDKs client.session() stops the browser when the block ends. With plain HTTP, stop it yourself, or it stops after the inactivity timeout. Don’t mix scrape calls and CDP automation in one browser: the scraper takes over the first tab. For more than ten input steps, use the SDK’s human emulation methods.