Python web scraping guide
Web scraping in Python with an API and Beautiful Soup
Beautiful Soup is good at reading HTML. It does not run JavaScript or retrieve pages that reject a basic HTTP client. This guide uses a web scraping API for retrieval and Beautiful Soup for parsing, so each tool does the job it is built for.
A typical Python scraper starts with requests.get(). That is enough for a static page, but it can return an empty shell when the useful content is rendered in the browser. Some sites also block ordinary HTTP clients. The split below is more dependable: retrieve the page through the API, then pass the returned HTML to Beautiful Soup.
What Beautiful Soup does, and what it does not
Beautiful Soup parses a document that you already have. Give it HTML and it can find headings, links, tables, metadata, or any element matched by a CSS selector. It does not open a browser, execute page scripts, solve access problems, or fetch the document for you.
That distinction explains a common failure. A script downloads a page with Requests, passes the response to Beautiful Soup, and finds no product cards or article text. The parser is working. The server simply returned HTML that expects JavaScript to fill in the page later.
Install Requests and Beautiful Soup
This example uses Requests for the API call and Beautiful Soup 4 for HTML parsing.
python -m pip install requests beautifulsoup4Keep the API key in a server-side environment variable. Do not paste it into source code, browser JavaScript, screenshots, or a public repository.
# macOS or Linux
export WEB_BYPASS_API_KEY="your_api_key"
# Windows PowerShell
$env:WEB_BYPASS_API_KEY="your_api_key"Fetch a page through the web scraping API
Send the target URL to POST /v1/fetch. Auto mode is the practical default: it starts with the lower-cost path and uses the browser-capable path only when the first result is not usable.
import os
import requests
api_key = os.environ["WEB_BYPASS_API_KEY"]
response = requests.post(
"https://web-bypass.dev/v1/fetch",
headers={"Authorization": f"Bearer {api_key}"},
json={
"url": "https://example.com",
"mode": "auto",
},
timeout=90,
)
data = response.json()
if not response.ok or not data.get("success"):
error = data.get("error", {})
raise RuntimeError(f"Fetch failed: {error.get('code', response.status_code)}")
if data["content_encoding"] != "utf8":
raise RuntimeError("Expected a text response, but received binary content")
html = data["content"]
print(f"Fetched {data['final_url']} with {data['access']['used_mode']}")The response is JSON. For an HTML page, the HTML is in content. The response also records the final URL after redirects, the HTTP status from the target, which access mode succeeded, the request charge, and the remaining account balance.
Parse the returned HTML with Beautiful Soup
Build the soup from data["content"], not from the API response body itself. The API response body is a JSON envelope around the page content.
from bs4 import BeautifulSoup
from urllib.parse import urljoin
soup = BeautifulSoup(html, "html.parser")
title = soup.title.get_text(strip=True) if soup.title else "Untitled"
links = [
urljoin(data["final_url"], anchor["href"])
for anchor in soup.select("a[href]")
]
print(title)
for link in links[:10]:
print(link)urljoin() converts relative links such as /docs into absolute URLs. Use final_url as the base because the target may have redirected before the page was returned.
A complete Python scraper
The complete version below returns a small, useful record instead of dumping a large HTML document to the terminal. It extracts the page title, meta description, links, final URL, status code, access mode, and charge.
import os
from urllib.parse import urljoin
import requests
from bs4 import BeautifulSoup
API_URL = "https://web-bypass.dev/v1/fetch"
API_KEY = os.environ["WEB_BYPASS_API_KEY"]
def scrape_page(url: str) -> dict:
response = requests.post(
API_URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={"url": url, "mode": "auto"},
timeout=90,
)
data = response.json()
if not response.ok or not data.get("success"):
error = data.get("error", {})
code = error.get("code", f"HTTP_{response.status_code}")
raise RuntimeError(f"Web scraping API error: {code}")
if data["content_encoding"] != "utf8":
raise RuntimeError("This scraper expects an HTML text response")
soup = BeautifulSoup(data["content"], "html.parser")
title = soup.title.get_text(" ", strip=True) if soup.title else "Untitled"
description_tag = soup.select_one('meta[name="description"]')
description = description_tag.get("content", "").strip() if description_tag else ""
links = [
urljoin(data["final_url"], anchor["href"])
for anchor in soup.select("a[href]")
]
return {
"title": title,
"description": description,
"links": links,
"final_url": data["final_url"],
"status_code": data["status_code"],
"used_mode": data["access"]["used_mode"],
"charge_usd": data["billing"]["charge_usd"],
}
result = scrape_page("https://example.com")
print(result)Start with a public page you control or a site that permits automated access. Check its terms and robots policy before collecting content at scale.
Choose Auto, Easy, or Hard mode
Most applications should keep mode set to auto. Direct mode selection is useful when you already know how the target behaves.
Tries Easy first. If that result fails validation and Hard is available, it tries Hard. Only the successful path is charged.
Best for ordinary public HTML. It is faster and costs $0.10 per 1,000 successful requests.
Use for JavaScript-heavy pages or pages that reject basic HTTP clients. It costs $0.20 per 1,000 successful requests.
A final failed request costs $0. See the pricing page for the current billing rules.
Handle API errors without hiding the cause
Do not catch every exception and return an empty list. That makes a blocked page look like a page with no results. Log the request ID and stable error code instead. Never log the Authorization header.
| Status | Error code | What to do |
|---|---|---|
| 400 | INVALID_URL | Fix the URL or request body. Do not retry unchanged input. |
| 401 | INVALID_API_KEY | Check or rotate the server-side API key. |
| 402 | INSUFFICIENT_BALANCE | Add balance before sending more work. |
| 403 | TARGET_NOT_ALLOWED | The target is outside the allowed public-web boundary. |
| 429 | RATE_LIMITED | Queue requests and retry later with backoff. |
| 502 / 504 | FETCH_FAILED / FETCH_TIMEOUT | Retry a limited number of times with backoff. |
Text pages normally use content_encoding: "utf8". Binary data may be returned as Base64. This tutorial stops on Base64 because Beautiful Soup expects text, not an image, PDF, or other binary file.
Use web scraping responsibly
A working request is not permission to collect or republish data. Respect the target site's terms, robots instructions, copyright, privacy obligations, and rate limits. Do not use the service for private pages, credentialed sessions, account access, or attempts to bypass authentication.
The API accepts public HTTP and HTTPS targets. Local, private, internal, and metadata-network addresses are rejected. Read the acceptable-use policy before putting a scraper into production.
Try the example
Get an API key, then run the script unchanged.
Create credentials on the home page, store the key in WEB_BYPASS_API_KEY, and replace https://example.com with a permitted public page.