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.- curl
- Python (async)
- JavaScript
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 ofdata 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.
The links of a front page
links are absolute and deduplicated. This example keeps only links to other sites.
A screenshot of a page
A screenshot is taken only whenformats 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 onPOST /scrape and on POST /profiles/{internal_uuid}/scrape.
Examples per option
Which wait to use
wait_untilsets when navigation is done. The defaultdomcontentloadedreturns once the HTML is parsed, before late images and scripts, so a page drawn by scripts can still be blank in a screenshot.loadwaits for those too.networkidlewaits for the network to go quiet and times out on pages with constant background traffic like analytics or chat widgets.commitreturns on the first response, enough for a status check.wait_forwaits 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.waitis a fixed pause. Use it only when nothing on the page signals it is ready, since it adds time to every request.actionsare for pages that show content only after input: a search, a cookie prompt, a “load more” button, a tab. Awaitaction between steps waits for the result of the previous one.
- Load
urluntilwait_until, withintimeout. - Pause for
waitseconds, then wait forwait_for, withintimeout. - Run
actionsin order. - Capture
formats.
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. Withauto_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.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 ownwait_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.