// field guide 002
Call a complete Bing SERP API with Python and httpx
Search Bing over a lightweight HTTP API and receive organic results plus query-dependent rich SERP groups, Knowledge Graph cards, and native pagination.
The client below sends one authenticated HTTP request and receives a normalized Bing result page. Organic links, rich SERP units, Knowledge Graph cards, and pagination arrive in the same JSON response.
01 / outcome
What you will receive
Save the complete file as bing_serp_api.py, export the free bearer key from the Litescrape workbench, and run a query. The direct endpoint returns ranked organic rows alongside whichever answer boxes, shopping units, local units, stories, images, videos, recipes, events, questions, or Knowledge Graph cards Bing supplied.
02 / transport
Use the reproduced HTTP surface
Litescrape requests Bing directly and parses the returned markup deterministically. Your application only needs an ordinary GET with query parameters and a bearer header. There is no navigation runtime, page lifecycle, selector code, or client-side concurrency fleet to manage.
03 / setup
Install httpx and set your key
Use Python 3.11 or newer. The key generated in the homepage workbench starts with ten free calls and works against the direct Maps, Reviews, and Bing endpoints.
python3 -m venv .venvsource .venv/bin/activatepython -m pip install --upgrade pip httpxexport LITESCRAPE_API_KEY='ls_live_…'04 / request contract
Send only the controls Bing understands
q is required, rejects control characters, and may contain up to 2,048 characters, including Bing search operators. location, lat, and lon localize the search; either coordinate may be supplied independently. Choose mkt or cc, never both. first is a one-based organic offset.
| Parameter | Default | Accepted values |
|---|---|---|
engine | bing | bing |
q | Required | 1–2,048 characters, no controls |
location | Empty | Up to 256 characters, no controls |
lat, lon | Empty | -90–90 and -180–180 |
mkt, cc | Empty | Market or country, mutually exclusive |
first | 1 | 1 through 9,007,199,254,740,991 |
safeSearch | moderate | off, moderate, strict |
filters | Empty | Up to 8,192 characters, no controls |
device | desktop | desktop, tablet, mobile |
import osimport httpx response = httpx.get( "https://api.litescrape.com/api/bing/search", params={ "engine": "bing", "q": "pizza shops in New York", "mkt": "en-US", "first": 1, "safeSearch": "moderate", "device": "desktop", }, headers={ "Authorization": f"Bearer {os.environ['LITESCRAPE_API_KEY']}", }, timeout=60,)response.raise_for_status()data = response.json() for result in data.get("organic_results", []): print(result["position"], result["title"], result["link"]) if knowledge := data.get("knowledge_graph"): print(knowledge.get("title"), knowledge.get("description"))05 / complete client
The complete Bing SERP API client
This file builds the exact public parameters, sends bearer authentication, preserves the stable error code, request ID, and retry flag, and prints organic and Knowledge Graph results. Its command-line flags expose the same inputs as the homepage workbench.
from __future__ import annotations import argparseimport osfrom typing import Any import httpx BING_ENDPOINT = "https://api.litescrape.com/api/bing/search" class LitescrapeApiError(RuntimeError): """A public Litescrape error with stable request metadata.""" def __init__( self, message: str, *, status_code: int, error_code: str, request_id: str = "", retryable: bool = False, ) -> None: super().__init__(message) self.status_code = status_code self.error_code = error_code self.request_id = request_id self.retryable = retryable def build_params( query: str, *, location: str = "", lat: float | None = None, lon: float | None = None, mkt: str = "", cc: str = "", first: int = 1, safe_search: str = "moderate", filters: str = "", device: str = "desktop",) -> dict[str, str | int | float]: query = query.strip() if not query: raise ValueError("query cannot be blank") params: dict[str, str | int | float] = { "engine": "bing", "q": query, "first": first, "safeSearch": safe_search, "device": device, } optional = { "location": location, "lat": lat, "lon": lon, "mkt": mkt, "cc": cc, "filters": filters, } params.update({name: value for name, value in optional.items() if value is not None and value != ""}) return params def search_bing( query: str, *, api_key: str, client: httpx.Client | None = None, timeout: float = 60, **parameters: Any,) -> dict[str, Any]: """Call the HTTP-only Bing Search API and return its structured JSON.""" if not api_key: raise ValueError("api_key cannot be blank") owns_client = client is None active_client = client or httpx.Client(timeout=timeout) try: response = active_client.get( BING_ENDPOINT, params=build_params(query, **parameters), headers={"Authorization": f"Bearer {api_key}"}, ) finally: if owns_client: active_client.close() if response.is_error: try: body = response.json() except ValueError: body = {} raise LitescrapeApiError( str(body.get("error") or "The Bing request failed."), status_code=response.status_code, error_code=str(body.get("error_code") or "request_failed"), request_id=str(body.get("request_id") or ""), retryable=body.get("retryable") is True, ) body = response.json() if not isinstance(body, dict): raise LitescrapeApiError( "The Bing API returned an unexpected response.", status_code=502, error_code="invalid_response", retryable=True, ) return body def main() -> None: parser = argparse.ArgumentParser(description="Search Bing through Litescrape.") parser.add_argument("query") parser.add_argument("--location", default="") parser.add_argument("--lat", type=float) parser.add_argument("--lon", type=float) parser.add_argument("--mkt", default="") parser.add_argument("--cc", default="") parser.add_argument("--first", type=int, default=1) parser.add_argument("--safe-search", choices=("off", "moderate", "strict"), default="moderate") parser.add_argument("--filters", default="") parser.add_argument("--device", choices=("desktop", "tablet", "mobile"), default="desktop") args = parser.parse_args() results = search_bing( args.query, api_key=os.environ["LITESCRAPE_API_KEY"], location=args.location, lat=args.lat, lon=args.lon, mkt=args.mkt, cc=args.cc, first=args.first, safe_search=args.safe_search, filters=args.filters, device=args.device, ) for result in results.get("organic_results", []): print(result.get("position"), result.get("title"), result.get("link")) knowledge = results.get("knowledge_graph") if isinstance(knowledge, dict): print("Knowledge Graph:", knowledge.get("title"), knowledge.get("description")) if __name__ == "__main__": main()06 / result groups
Read the groups Bing actually returned
Start with organic_results, then inspect optional units such as answer_box, answer_box_list, carousel_results, local_results, inline_shopping_results, short_videos, related_questions, top_stories, and top_shopping_results. Their names and nesting follow the Bing Search API reference, while absent groups stay absent rather than becoming empty invented values.
import osimport httpx response = httpx.get( "https://api.litescrape.com/api/bing/search", params={ "engine": "bing", "q": "pizza shops in New York", "mkt": "en-US", "first": 1, "safeSearch": "moderate", "device": "desktop", }, headers={ "Authorization": f"Bearer {os.environ['LITESCRAPE_API_KEY']}", }, timeout=60,)response.raise_for_status()data = response.json() for result in data.get("organic_results", []): print(result["position"], result["title"], result["link"]) if knowledge := data.get("knowledge_graph"): print(knowledge.get("title"), knowledge.get("description"))07 / knowledge graph
Handle primary and repeated Knowledge Graph cards
knowledge_graph is the primary card. knowledge_graph_list contains repeated local entities or disambiguated cards and may coexist with it. Common fields cover identity, media, profiles, facts, people, local entities, entertainment, travel, and food. Nested sections such as compare, country_facts, hours, reviews_ratings, and nutrition_facts keep their native dynamic keys, so iterate them rather than hard-coding every label.
08 / continuation
Follow pagination
Use litescrape_pagination.next when it is present to call the next Litescrape page with the original request context and the next native first offset. pagination.next remains the corresponding native Bing URL.
09 / errors
Branch on stable errors, not message text
| Status | Meaning | Response |
|---|---|---|
400 invalid_request | Parameter contract conflict | Fix the request before retrying |
401 | Missing or invalid key | Replace the bearer credential |
402 payment_required | No calls remain | Top up the same key |
422 validation_error | Typed value cannot be coerced | Correct that field |
503 service_unavailable | The request could not be completed right now | Retry after the Retry-After delay when retryable is true |
Log request_id with your job. Invalid requests are rejected before a call is reserved; failures after worker dispatch are refunded, and only a successful response consumes one.