<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SerpApi</title>
    <description>The latest articles on DEV Community by SerpApi (serpapi).</description>
    <link>https://dev.to/serpapi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2993%2F7009fcb6-41c1-44c2-b881-75eef15b2791.png</url>
      <title>DEV Community: SerpApi</title>
      <link>https://dev.to/serpapi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/serpapi"/>
    <language>en</language>
    <item>
      <title>DeepSeek Search and Fetch APIs vs. SerpApi: Features, Pricing, and Use Cases</title>
      <dc:creator>Magenta Qin</dc:creator>
      <pubDate>Sun, 08 Nov 2026 20:16:15 +0000</pubDate>
      <link>https://dev.to/serpapi/deepseek-search-and-fetch-apis-vs-serpapi-features-pricing-and-use-cases-1lod</link>
      <guid>https://dev.to/serpapi/deepseek-search-and-fetch-apis-vs-serpapi-features-pricing-and-use-cases-1lod</guid>
      <description>&lt;p&gt;DeepSeek has opened the web search infrastructure behind its models to developers. Its new &lt;a href="https://api-docs.deepseek.com/guides/search/" rel="noopener noreferrer"&gt;Search and Fetch APIs&lt;/a&gt; give agents and retrieval-augmented generation (RAG) applications a way to find live web pages and turn them into model-ready context.&lt;/p&gt;

&lt;p&gt;That inevitably raises a question for developers already using search and web extraction APIs: &lt;strong&gt;can DeepSeek Search and Fetch API replace SerpApi?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The short answer is that the two products have overlapping features, but they operate at different layers of the search stack. DeepSeek Search is an AI-native web retrieval API. It is designed to find relevant pages, rank them using signals such as relevance and authority, and return summaries that a language model can read directly. SerpApi provides structured search data. It gives applications access to results from Google, Bing, Baidu, Google Maps, Google Shopping, YouTube, Google Flights, and many other general and vertical search products.&lt;/p&gt;

&lt;p&gt;Both can power AI agents, but they provide agents with different kinds of context. DeepSeek Search emphasizes ranked web pages and model-ready summaries, while SerpApi can provide source-specific, structured data such as flight options, local business details, and shopping results. The right choice depends on whether the agent needs general web evidence or data from a particular search product.&lt;/p&gt;

&lt;p&gt;This blog will compare their features, pricing, rate limits, and best use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What DeepSeek Released&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;DeepSeek introduced two endpoints for live web retrieval:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search API:&lt;/strong&gt; finds web pages for a query and returns a ranked list of structured results. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetch API:&lt;/strong&gt; retrieves a specific public web page and returns its main content as Markdown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A basic search request looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://api.deepseek.com/v1/tools/search &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "query": "中国 AI 编程工具对比 2026",
    "count": 5,
    "need_content": true
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API returned the following result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GLM、MiniMax、Kimi、豆包、千问，国产模型Coding Plan怎么选？一文全套短评"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"site_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tencent.cn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"authority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rerank_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.226604461669922&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-02T16:00:00+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cloud.tencent.cn/developer/article/2633452#1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"最近使用 Claude Code 编程，结合 Skills，完成一些新的项目，也趁此机会测试了国产模型编程方面的效果……"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;DeepSeek Search returns at most 50 results in one response, and its current documentation does not expose a pagination parameter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihmocl8oghz3esrxghzq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fihmocl8oghz3esrxghzq.png" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By contrast, SerpApi supports pagination on supported search engines. For example, Google Search responses can include&lt;code&gt;serpapi_pagination.next&lt;/code&gt;, and the &lt;code&gt;start&lt;/code&gt; parameter can be used to request subsequent result pages. See &lt;a href="https://serpapi.com/pagination" rel="noopener noreferrer"&gt;SerpApi’s pagination documentation&lt;/a&gt; for examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How SerpApi Approaches Search&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;SerpApi takes a different approach. Instead of maintaining one general-purpose search index, it turns existing search engines and vertical search products into structured APIs.&lt;/p&gt;

&lt;p&gt;A Google Search request, for example, can return separate sections for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organic results&lt;/li&gt;
&lt;li&gt;Answer boxes&lt;/li&gt;
&lt;li&gt;Knowledge Graph data&lt;/li&gt;
&lt;li&gt;Local results&lt;/li&gt;
&lt;li&gt;News, images, and videos&lt;/li&gt;
&lt;li&gt;Shopping results&lt;/li&gt;
&lt;li&gt;Related questions and searches&lt;/li&gt;
&lt;li&gt;Ads and other result-page features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same pattern extends beyond general web search. SerpApi provides dedicated APIs for products such as Google Maps, Google Shopping, Google Flights, Google Hotels, YouTube, Amazon, Apple App Store, and many others. Each API exposes fields specific to its source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That difference becomes important when an agent needs reliable fields it can act on, not only prose it can read.&lt;/strong&gt; A local-search workflow may need a business's rating, review count, address, phone number, opening hours, and location on a map. A flight-search workflow may need airports, departure times, layovers, duration, airline, carbon emissions, and price. &lt;strong&gt;Those are structured business fields, not just webpage summaries.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SerpApi lets developers define where a search is performed, which country and language it targets, and whether it runs on desktop or mobile. Engine-specific parameters can further filter or sort the results. These controls make searches more targeted and reproducible, rather than relying mainly on the caller's IP address.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkkhf3j046etubr7n5zpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkkhf3j046etubr7n5zpj.png" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Many SerpApi engines also expose pagination. For Google Search, for example, the response can include&lt;code&gt;serpapi_pagination.next&lt;/code&gt;, while the &lt;code&gt;start&lt;/code&gt; parameter can request subsequent result pages. The number of available pages still depends on what the underlying search engine serves, and each page is a separate SerpApi search. See the Google Search API documentationhttps://serpapi.com/search-api) for an example of the available parameters and response types.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Core Difference: Web Retrieval vs. Search Product Data&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The simplest way to distinguish the two products is to ask what the agent or application ultimately needs.&lt;/p&gt;

&lt;p&gt;Both DeepSeek Search API and SerpApi can return relevant web pages that help a model answer a question. &lt;strong&gt;DeepSeek's distinction is that every result includes model-oriented retrieval signals:&lt;/strong&gt; a long summary, optional page content, &lt;code&gt;rerank_score&lt;/code&gt;and an &lt;code&gt;authority&lt;/code&gt; tier. These fields can help an agent decide which sources to read first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SerpApi's distinction is its breadth of sources and structured result types.&lt;/strong&gt; An agent can search SerpApi's own web index or retrieve data from a particular engine or vertical product. Instead of flattening every result into generic text, SerpApi &lt;strong&gt;preserves source-specific fields&lt;/strong&gt;. For example, a Maps result can include ratings and opening hours, while a Flights result can include prices, schedules, and layovers. SerpApi's Search Index documentation does not currently expose an equivalent to DeepSeek's &lt;code&gt;authority&lt;/code&gt; field, but it provides pagination, localization, multiple content types, and source-specific controls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcor2rmziszfo7pkybole.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcor2rmziszfo7pkybole.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO Use Case DeepSeek Does Not Cover: Cross-Border E-commerce Monitoring
&lt;/h2&gt;

&lt;p&gt;A brand selling internationally needs data from the marketplaces and shopping engines where customers actually search, not just a list of relevant pages from the general web.&lt;/p&gt;

&lt;p&gt;Consider a Chinese consumer-electronics brand expanding into the United States and Germany. It may need to track where its products appear for a keyword on Google Shopping, Amazon, and Walmart; compare prices and ratings with competitors; distinguish sponsored from organic listings; and repeat the same searches for different countries, languages, and devices.&lt;/p&gt;

&lt;p&gt;SerpApi supports this type of workflow. Its dedicated &lt;a href="https://serpapi.com/google-shopping-api" rel="noopener noreferrer"&gt;Google Shopping&lt;/a&gt;, &lt;a href="https://serpapi.com/amazon-search-api" rel="noopener noreferrer"&gt;Amazon&lt;/a&gt;, and &lt;a href="https://serpapi.com/walmart-search-api" rel="noopener noreferrer"&gt;Walmart&lt;/a&gt; APIs contain source-specific fields such as product position, price, rating, review count, seller, and product identifiers. The application can store the structured responses and compare changes across markets and over time.&lt;/p&gt;

&lt;p&gt;DeepSeek Search can still support market research by finding product reviews, news, or competitor websites. &lt;strong&gt;However, its results come from a general web index rather than a specified marketplace.&lt;/strong&gt; Its &lt;code&gt;rerank_score&lt;/code&gt; is not an Amazon product rank or a Google Shopping position, and it does not return a stable marketplace-specific product schema. DeepSeek Fetch API can read a known product page, but the application would still need to extract and normalize fields such as price, seller, and rating itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For marketplace rank tracking, international price monitoring, and cross-border competitor analysis, SerpApi therefore provides structured source data that DeepSeek Search and Fetch API cannot replace.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Developer Experience: Raw HTTP vs. Official Client Libraries&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;DeepSeek Search and Fetch are raw HTTP APIs, not language-specific SDKs. The documentation shows cURL and Python&lt;code&gt;requests&lt;/code&gt;, but they can be called from any language with an HTTP client. Without a dedicated SDK, teams may need to handle authentication, retries, errors, and response types themselves.&lt;/p&gt;

&lt;p&gt;SerpApi supports raw HTTP and provides &lt;a href="https://serpapi.com/libraries" rel="noopener noreferrer"&gt;official client libraries&lt;/a&gt; for Ruby, Python, JavaScript/TypeScript, PHP, Go, Java, .NET, Swift, and C++. These libraries can reduce integration and maintenance work through language-native helpers for common API operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search and Page Extraction
&lt;/h2&gt;

&lt;p&gt;DeepSeek Fetch and SerpApi's Extract API have the most direct overlap in features. DeepSeek Fetch accepts a URL and returns its title and content as Markdown. Here is how both APIs can extract the same page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# DeepSeek Fetch&lt;/span&gt;
curl https://api.deepseek.com/v1/tools/fetch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url":"https://cloud.tencent.cn/developer/article/2633452"}'&lt;/span&gt;

&lt;span class="c"&gt;# SerpApi Extract with Markdown output&lt;/span&gt;
curl &lt;span class="nt"&gt;--get&lt;/span&gt; https://serpapi.com/search &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"engine=extract"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"url=https://cloud.tencent.cn/developer/article/2633452"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"output=md"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"api_key=&lt;/span&gt;&lt;span class="nv"&gt;$SERPAPI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DeepSeek Fetch returned a JSON object containing the page title and Markdown content. The response below is shortened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GLM、MiniMax、Kimi、豆包、千问，国产模型Coding Plan怎么选？一文全套短评"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"# GLM、MiniMax、Kimi、豆包、千问，国产模型Coding Plan怎么选？&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;最近使用 Claude Code 编程，结合 Skills，完成一些新的项目……"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the &lt;a href="https://serpapi.com/extract-api" rel="noopener noreferrer"&gt;Extract API&lt;/a&gt; and &lt;code&gt;output=md&lt;/code&gt;SerpApi returned Markdown directly. This response is also shortened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;www.tangshuang.net

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;社区首页&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://cloud.tencent.cn/developer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;专栏&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://cloud.tencent.cn/developer/column&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="gh"&gt;# GLM、MiniMax、Kimi、豆包、千问，国产模型Coding Plan怎么选？一文全套短评&lt;/span&gt;

发布于 2026-03-03 16:09:25

Hello大家好呀！最近使用 Claude Code 编程，结合 Skills，完成一些新的项目，
也趁此机会测试了国产模型编程方面的效果……
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the responses above, we can see that &lt;strong&gt;DeepSeek focused more tightly on the article's main content. At the same time, SerpApi preserved more of the original page structure&lt;/strong&gt; , including navigation links and page-level metadata. The difference here is the extraction scope, not just the output format: SerpApi can also return JSON, but in this test it retained a broader set of page elements than DeepSeek Fetch did.&lt;/p&gt;

&lt;p&gt;SerpApi's &lt;a href="https://serpapi.com/extract-api" rel="noopener noreferrer"&gt;Extract API&lt;/a&gt; also supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scrape&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, and &lt;code&gt;crawl&lt;/code&gt; modes&lt;/li&gt;
&lt;li&gt;JSON, Markdown, and HTML output&lt;/li&gt;
&lt;li&gt;Desktop, tablet, and mobile browser profiles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It follows &lt;code&gt;robots.txt&lt;/code&gt; and uses the appropriate SerpApi engine when the URL is a supported search page, such as Google/Amazon or Bing/Baidu.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;JSON for Code, Markdown for Models&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;DeepSeek Search and Fetch API are both JSON APIs. Search returns a &lt;code&gt;results&lt;/code&gt; array, while Fetch returns &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;content&lt;/code&gt;. Fetch API stores Markdown in the &lt;code&gt;content&lt;/code&gt; field, but the response itself is still JSON. The current &lt;a href="https://api-docs.deepseek.com/guides/search/" rel="noopener noreferrer"&gt;DeepSeek documentation&lt;/a&gt;does not describe a direct Markdown response mode for either API.&lt;/p&gt;

&lt;p&gt;SerpApi offers several response formats. JSON remains useful when application code needs exact fields and a strict schema. When search results are sent directly to an LLM, however, Markdown can represent the same evidence with less formatting overhead.&lt;/p&gt;

&lt;p&gt;JSON repeats field names for every result and adds braces, quotes, commas, and nested objects. SerpApi's &lt;a href="https://serpapi.com/markdown-output" rel="noopener noreferrer"&gt;Markdown output&lt;/a&gt; turns result arrays into tables, uses native Markdown links, and removes internal tracking fields, opaque tokens, and duplicated values. SerpApi reports roughly 50% average token savings compared with its JSON responses, although the result varies by API and query. Complex nested responses can see savings above 90%; use JSON when the application needs fields omitted from the curated Markdown view.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://serpapi.com/blog/from-json-to-markdown-reducing-ai-seo-analysis-token-costs-by-90/" rel="noopener noreferrer"&gt;Google Shopping experiment&lt;/a&gt; explains the difference. Across 12 queries, Markdown used 89.55% fewer input tokens on average. With a fixed 1,550-token budget, the JSON representation fit 16 products, while Markdown fit all 30. Markdown did not improve the model itself; it allowed it to compare more of the original evidence within the same context window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pricing and Rate Limits
&lt;/h3&gt;

&lt;p&gt;DeepSeek's pricing is relatively low for a web search API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Published rate limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek Search&lt;/td&gt;
&lt;td&gt;$0.75 per 1,000 successful requests&lt;/td&gt;
&lt;td&gt;100 QPS per account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek Fetch&lt;/td&gt;
&lt;td&gt;$0.75 per 1,000 successful requests&lt;/td&gt;
&lt;td&gt;25 QPS per account(per-domain QPS limit is not publicly documented)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SerpApi pricing is based on monthly plans. Each plan includes a monthly search quota and an hourly throughput limit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Monthly price&lt;/th&gt;
&lt;th&gt;Searches per month&lt;/th&gt;
&lt;th&gt;Throughput per hour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer&lt;/td&gt;
&lt;td&gt;$75&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;$150&lt;/td&gt;
&lt;td&gt;15,000&lt;/td&gt;
&lt;td&gt;3,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Big Data&lt;/td&gt;
&lt;td&gt;$275&lt;/td&gt;
&lt;td&gt;30,000&lt;/td&gt;
&lt;td&gt;6,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DeepSeek is much cheaper per request, but &lt;strong&gt;the cheapest request does not always produce the lowest total cost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl25lngfjic4jca7a4qts.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl25lngfjic4jca7a4qts.png" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For general research, DeepSeek may return the sources and summaries an agent needs in one request. For product monitoring, SerpApi may cost more per request but return prices, sellers, and ratings directly, avoiding extra page fetches and model-based extraction.&lt;/p&gt;

&lt;p&gt;The real cost therefore depends on the number of API calls, the model tokens consumed, and any additional processing required. We will compare these factors in the follow-up benchmark blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Choosing Between DeepSeek and SerpApi&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Choose DeepSeek Search/Fetch API when&lt;/strong&gt; :
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;You need to search the general web and read a few relevant pages, rather than relying on a specific search engine/product such as Google Maps or Google Shopping.&lt;/li&gt;
&lt;li&gt;Built-in summaries, &lt;code&gt;rerank_score&lt;/code&gt;, &lt;code&gt;authority&lt;/code&gt;and optional page content are more useful than source-specific fields.&lt;/li&gt;
&lt;li&gt;You want a low-cost API and do not need more than 50 results or pagination.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Typical uses include research assistants, technical-documentation discovery, and RAG source collection.&lt;/p&gt;

&lt;h3&gt;
  
  
  When SerpApi Is the Better Fit
&lt;/h3&gt;

&lt;p&gt;SerpApi is a stronger fit when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need results from a specific source, such as Google, Bing, Google Maps, Google Shopping, YouTube, or Google Flights.&lt;/li&gt;
&lt;li&gt;Your code needs structured fields such as prices, ratings, opening hours, or flight schedules.&lt;/li&gt;
&lt;li&gt;You need precise control over location, language, country, domain, or device.&lt;/li&gt;
&lt;li&gt;You are building rank tracking, price monitoring, local search, travel search, or marketplace research.&lt;/li&gt;
&lt;li&gt;You need features such as pagination, raw HTML, archived searches, asynchronous requests, or site crawling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Use them together&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A product does not have to use the same API for every search. It can route each request based on what the task requires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Simple web research:&lt;/strong&gt; Use DeepSeek Search when general web results are enough, and the task can be completed within its 50-result limit. Use DeepSeek Fetch API to read the most relevant pages.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Larger or more controlled searches:&lt;/strong&gt; Use SerpApi when the product needs pagination, precise location or device settings, or results from a specific search engine.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source-specific product features:&lt;/strong&gt; Use SerpApi's structured JSON for current rankings, price monitoring, local search, travel search, or data from sources such as Google, Amazon, and Instagram.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI analysis:&lt;/strong&gt; DeepSeek's summaries and fetched content can be passed to a model. SerpApi results can also be requested as Markdown and sent directly to the model, without first converting a large JSON response into readable text. For example, a market-intelligence product could use DeepSeek to discover new competitors across the web, SerpApi to monitor their Amazon listings and Instagram profiles, and SerpApi's Markdown output to provide an AI agent with concise context for its final analysis.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;At-a-glance comparison&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;DeepSeek Search and Fetch API&lt;/th&gt;
&lt;th&gt;SerpApi&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main focus&lt;/td&gt;
&lt;td&gt;General-purpose web search and page retrieval&lt;/td&gt;
&lt;td&gt;Engine-specific search across 100+ search engines and verticals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search source&lt;/td&gt;
&lt;td&gt;DeepSeek's general web index&lt;/td&gt;
&lt;td&gt;The caller selects an &lt;code&gt;engine&lt;/code&gt;, such as &lt;code&gt;search_index&lt;/code&gt;, &lt;code&gt;google&lt;/code&gt;, &lt;code&gt;amazon&lt;/code&gt;, &lt;code&gt;google_maps&lt;/code&gt;, or &lt;code&gt;google_flights&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result structure&lt;/td&gt;
&lt;td&gt;Web results with summaries and optional page content&lt;/td&gt;
&lt;td&gt;Structured fields tailored to each search engine or vertical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ranking signals&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rerank_score&lt;/code&gt; and &lt;code&gt;authority&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Result position and source-specific fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Page retrieval&lt;/td&gt;
&lt;td&gt;Search can include page content with &lt;code&gt;need_content&lt;/code&gt;; Fetch reads one known URL and returns its title and main content; no documented site mapping or crawling&lt;/td&gt;
&lt;td&gt;Extract can scrape one page, map a site's URLs, or crawl up to 100 pages; crawl mode uses one credit per successfully extracted page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pagination&lt;/td&gt;
&lt;td&gt;No documented pagination; up to 50 results&lt;/td&gt;
&lt;td&gt;Available on supported engines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search controls&lt;/td&gt;
&lt;td&gt;Localization IP and query operators&lt;/td&gt;
&lt;td&gt;Location, country, language, domain, device, and source-specific parameters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response formats&lt;/td&gt;
&lt;td&gt;JSON; extracted page content is stored as Markdown inside the JSON response&lt;/td&gt;
&lt;td&gt;JSON, Markdown, and HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client libraries&lt;/td&gt;
&lt;td&gt;Raw HTTP; no dedicated Search/Fetch SDK suite&lt;/td&gt;
&lt;td&gt;Official libraries for nine languages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing&lt;/td&gt;
&lt;td&gt;$0.75 per 1,000 successful Search requests; $0.75 per 1,000 successful Fetch requests&lt;/td&gt;
&lt;td&gt;Monthly plans: $0 for 250, $25 for 1,000, $75 for 5,000, $150 for 15,000, or $275 for 30,000 searches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limits&lt;/td&gt;
&lt;td&gt;Search: 100 QPS per account; Fetch: 25 QPS per account plus an unpublished per-domain limit&lt;/td&gt;
&lt;td&gt;50 to 6,000 searches per hour, depending on the plan; custom limits for Enterprise&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;DeepSeek Search and Fetch API provide a low-cost way to find information across the general web and read selected pages. SerpApi covers a broader range of sources and use cases. It combines its own web index with APIs for specific search engines and vertical products, preserving fields such as prices, ratings, opening hours, and flight details. Its JSON, Markdown, and HTML responses also support both application code and AI workflows.&lt;/p&gt;

&lt;p&gt;So, can DeepSeek Search and Fetch replace SerpApi?&lt;/p&gt;

&lt;p&gt;DeepSeek can be simpler and less expensive. But it is not a like-for-like replacement when a product needs results from a specific source, pagination, precise localization, or source-specific structured fields.&lt;/p&gt;

&lt;p&gt;The more useful question is not which API is universally better. It is whether the task needs &lt;strong&gt;compact general web retrieval&lt;/strong&gt; or &lt;strong&gt;source-specific, structured search data&lt;/strong&gt;. Some products will need both.&lt;/p&gt;

&lt;p&gt;In the next blog, we will test both APIs on breaking news, local business search, product research, and technical research. We will compare relevance, latency, output size, rate-limit behavior, and the actual cost of completing each task.&lt;/p&gt;

&lt;h1&gt;
  
  
  DeepSeek #Exract API
&lt;/h1&gt;

</description>
      <category>api</category>
      <category>llm</category>
      <category>rag</category>
      <category>search</category>
    </item>
    <item>
      <title>Markdown vs. JSON: Which Response Format Should You Use?</title>
      <dc:creator>Sonika Arora</dc:creator>
      <pubDate>Sun, 08 Nov 2026 04:10:01 +0000</pubDate>
      <link>https://dev.to/serpapi/markdown-vs-json-which-response-format-should-you-use-7l5</link>
      <guid>https://dev.to/serpapi/markdown-vs-json-which-response-format-should-you-use-7l5</guid>
      <description>&lt;p&gt;When using APIs to get search results, the format you choose can impact how easily your application uses that data. Markdown and JSON both let you work with search results, but they're designed for different needs. JSON organizes information into structured fields that your code can access directly. Markdown presents information in a readable, text-based format that's easy for people and language models to interpret.&lt;/p&gt;

&lt;p&gt;The right choice depends on what happens &lt;em&gt;after&lt;/em&gt; you retrieve your search results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown vs. JSON at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;The short answer&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
➡️ Choose Markdown when the next step is primarily reading, displaying, or reasoning, especially when the output feeds an LLM and token cost matters.&lt;br&gt;&lt;br&gt;
➡️ Choose JSON when the next step is primarily processing or manipulating data, such as extracting fields, validating, or storing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SerpApi's APIs offer responses in both formats so you can pick the best one that fits into your workflow. Let’s dive into how Markdown and JSON compare across real-world SerpApi integration scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example use cases that benefit from markdown
&lt;/h2&gt;

&lt;p&gt;Markdown is a lightweight text format that supports headings, lists, links, and other simple formatting. For search results, this makes Markdown particularly useful when you want to give an AI model or agent information it can understand and use to complete a task.&lt;/p&gt;

&lt;p&gt;You can read more about the benefit of Markdown output from our APIs here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/markdown-output" rel="noopener noreferrer"&gt;SerpApi: Markdown Output for AI Agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use case 1: Building AI agents that research and answer questions
&lt;/h3&gt;

&lt;p&gt;Imagine you're building an AI research agent that answers questions using live web search.&lt;/p&gt;

&lt;p&gt;A user asks: "What are the latest developments in the electric vehicle market, and how are the leading manufacturers responding?"&lt;/p&gt;

&lt;p&gt;Your agent needs to search the web, gather relevant results, compare sources, identify trends, and generate a coherent answer.&lt;/p&gt;

&lt;p&gt;Markdown is a natural fit for this workflow. Search results can be presented as readable text with titles, snippets, links, and other available information.&lt;/p&gt;

&lt;p&gt;Markdown works well here because the model can focus on understanding the information rather than interpreting a complex data structure, and search results can be passed into summarization, comparison, and question-answering workflows.&lt;/p&gt;

&lt;p&gt;Here’s an example of the Markdown output for the query &lt;code&gt;latest developments in the electric vehicle market&lt;/code&gt; using Google News API that gets the latest news on that:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5ceh5qjrpi2htu6yrsp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5ceh5qjrpi2htu6yrsp.png" alt="a screenshot of the playground example" width="800" height="503"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;a screenshot of the playground example&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Try it in the playground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/playground?engine=google_news&amp;amp;q=latest+developments+in+the+electric+vehicle+market&amp;amp;gl=us&amp;amp;hl=en" rel="noopener noreferrer"&gt;SerpApi Playground - SerpApi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 If you see the JSON output in the playground and want to switch to Markdown, head over to the tools button in the top right of the output window and toggle over to Markdown 👇&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4wk63eo8kblvxb2cg7km.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4wk63eo8kblvxb2cg7km.png" alt="Switch between JSON and Markdown" width="798" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Switch between JSON and Markdown&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Use case 2: Retrieval-augmented generation (RAG)
&lt;/h3&gt;

&lt;p&gt;Retrieval-augmented generation, or RAG, combines information retrieved from external sources with an LLM's ability to generate answers.&lt;/p&gt;

&lt;p&gt;For example, you might build an assistant that retrieves current web information before answering questions about competitors, industry trends, or product specifications - Markdown works well in this workflow.&lt;/p&gt;

&lt;p&gt;This is because the retrieved information is intended to be read and interpreted by a language model. Choose Markdown when search is one step in a larger reasoning workflow and the agent needs to interpret the results before taking action.&lt;/p&gt;

&lt;p&gt;Here’s an example of the Markdown output for the query &lt;code&gt;iPhone&lt;/code&gt; using Google Shopping API that gets all the shopping results in a simple, easy-to-analyze format:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19n2k3b32xk291w86mht.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F19n2k3b32xk291w86mht.png" alt="a screenshot of the playground example" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;a screenshot of the playground example&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Try it in the playground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/playground?engine=google_shopping&amp;amp;q=iPhone" rel="noopener noreferrer"&gt;SerpApi Playground - SerpApi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use case 3: Creating readable research summaries and reports
&lt;/h3&gt;

&lt;p&gt;You might be building a tool that generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Competitor research reports&lt;/li&gt;
&lt;li&gt;Market research summaries&lt;/li&gt;
&lt;li&gt;News briefings&lt;/li&gt;
&lt;li&gt;Product comparisons&lt;/li&gt;
&lt;li&gt;Research notes for analysts&lt;/li&gt;
&lt;li&gt;Summaries of relevant web sources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the end product is primarily readable text, Markdown is a practical intermediate format.&lt;/p&gt;

&lt;p&gt;Here’s an example of the Markdown output for the query &lt;code&gt;coffee&lt;/code&gt; using Google Maps API that gets all the coffee shops in a particular area. This can easily be compiled into a report by any AI agent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3qoxhhltpdzoro68jmno.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3qoxhhltpdzoro68jmno.png" alt="a screenshot of the playground example" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;a screenshot of the playground example&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Try it in the playground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/playground?engine=google_maps&amp;amp;q=Coffee&amp;amp;ll=%4040.7455096%2C-74.0083012%2C14z&amp;amp;hl=en&amp;amp;type=search" rel="noopener noreferrer"&gt;SerpApi Playground - SerpApi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example use cases that benefit from JSON
&lt;/h2&gt;

&lt;p&gt;JSON, or JavaScript Object Notation, is a structured data format. It organizes information into objects and arrays containing named fields and values.&lt;/p&gt;

&lt;p&gt;This works great when your application needs to identify specific pieces of information and do something with them programmatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use case 1: Building applications that display search results
&lt;/h3&gt;

&lt;p&gt;Say you're building a search interface that displays Google Shopping product comparisons inside your own application. You'll ideally want to display each result in a consistent layout with a title, link, snippet, image, or price, depending on the search engine and result type. JSON makes this straightforward.&lt;/p&gt;

&lt;p&gt;Your application can read the relevant fields and use them to populate UI components. Each result can become a card, row, or other interface element without having to extract information from formatted text.&lt;/p&gt;

&lt;p&gt;Here's an example of what the JSON output looks like from SerpApi's Google Shopping API for the query "laptop":&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnoi8w1istnzn205kre9q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnoi8w1istnzn205kre9q.png" alt="a screenshot of the playground example" width="799" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;a screenshot of the playground example&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your application can access &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, and &lt;code&gt;source&lt;/code&gt; directly and render them wherever needed.&lt;/p&gt;

&lt;p&gt;Try it in the playground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/playground?device=desktop&amp;amp;engine=google_shopping&amp;amp;google_domain=google.com&amp;amp;location=Austin%2C+Texas%2C+United+States&amp;amp;q=laptop" rel="noopener noreferrer"&gt;SerpApi Playground - SerpApi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use case 2: Using data in automated pipelines
&lt;/h3&gt;

&lt;p&gt;Consider an application that tracks flight prices across multiple airlines. The app needs to retrieve flight information, extract prices, compare them over time, and store the results for further analysis, all of which is set up in a series of steps.&lt;/p&gt;

&lt;p&gt;The results need to enter a repeatable data-processing pipeline, so JSON is well suited to this process. The application can access fields directly rather than trying to recover individual values from formatted text.&lt;/p&gt;

&lt;p&gt;Here's an example of what the JSON output looks like from SerpApi's Google Flights API for flights from CDG to AUS:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbi5dk8hmwi95qsgzmqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcbi5dk8hmwi95qsgzmqc.png" alt="a screenshot of the playground example" width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;a screenshot of the playground example&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Try it in the playground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/playground?engine=google_flights&amp;amp;departure_id=CDG&amp;amp;arrival_id=AUS&amp;amp;gl=us&amp;amp;hl=en&amp;amp;currency=USD&amp;amp;outbound_date=2026-10-30&amp;amp;return_date=2026-11-05" rel="noopener noreferrer"&gt;SerpApi Playground - SerpApi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use case 3: Monitoring, filtering, and triggering actions
&lt;/h3&gt;

&lt;p&gt;Say you're building an application that needs to compare specific values across requests. In this case, JSON makes it easier to access the fields required for that comparison, apply business rules, and trigger the next action.&lt;/p&gt;

&lt;p&gt;For example, if you're building a system that monitors Google Search rankings for a list of keywords, and you want to detect when a particular website enters the top five results, record its position, and send a Slack notification when its ranking changes. This logic is more straightforward when the application can access structured fields rather than interpret formatted text.&lt;/p&gt;

&lt;p&gt;The same approach works for monitoring job listings, tracking product availability, identifying new search results, or detecting changes in other supported search-engine data.&lt;/p&gt;

&lt;p&gt;Here's an example of the JSON output from SerpApi's Google Jobs API that you can use to track open jobs over time:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42fjikru939olbi3wg3v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42fjikru939olbi3wg3v.png" alt="a screenshot of the playground example" width="800" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;a screenshot of the playground example&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Try it in the playground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/playground?engine=google_jobs&amp;amp;q=Barista" rel="noopener noreferrer"&gt;SerpApi Playground - SerpApi&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Common Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can you use JSON with LLMs?
&lt;/h3&gt;

&lt;p&gt;JSON is not limited to traditional software applications. LLMs can interpret JSON, extract information from it, compare fields, and use it as context. In fact, JSON can be preferable when you need the model to reason about specific attributes, such as product prices, publication dates, or search positions.&lt;/p&gt;

&lt;p&gt;For example, if an LLM needs to compare three products by price and rating, a structured JSON representation makes those attributes explicit. Markdown may be more convenient and less token-intensive if the task involves reading several articles and synthesizing their arguments.The distinction is about the shape of the information and the task being performed.&lt;/p&gt;

&lt;p&gt;JSON doesn't guarantee that an LLM will produce correct answers, and Markdown doesn't guarantee better reasoning. Both formats still require appropriate context management and validation.&lt;/p&gt;

&lt;p&gt;Our APIs provide an option to receive the results in either JSON or Markdown format, so you can use the one that works best for your use case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Markdown use fewer tokens than JSON?
&lt;/h3&gt;

&lt;p&gt;Generally, yes - if you use it for the right tasks. Markdown can avoid a lot of the structural overhead associated with nested JSON objects and repeated field names.&lt;/p&gt;

&lt;p&gt;We've observed that using the Markdown version of the response from our APIs consumes ~50% fewer LLM tokens on average. All informational content is preserved: titles, snippets, links, ratings, prices, etc. Internal tracking fields (opaque tokens, redirect URLs, duplicate numeric extractions) are stripped to save tokens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6t17oi9yhh5pufe8t4v1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6t17oi9yhh5pufe8t4v1.png" alt="Markdown vs JSON: Token savings" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Markdown vs JSON: Token savings&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That means lower LLM costs, faster inference, and more of the context window free for your actual task.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can you switch between Markdown and JSON in the same application?
&lt;/h3&gt;

&lt;p&gt;Yes, you can switch between using JSON and Markdown for any SerpApi endpoint.&lt;/p&gt;

&lt;p&gt;JSON is the default output, so no additional parameters or settings are needed. Any search can return the Markdown output in three ways:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5fi8ddtijq2065gws3ot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5fi8ddtijq2065gws3ot.png" alt="Ways to get Markdown output" width="800" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ways to get Markdown output&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're using code, this looks like:&lt;/p&gt;

&lt;blockquote&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# All three produce the same Markdown output:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;

&lt;p&gt;curl "&lt;a href="https://serpapi.com/search?engine=google&amp;amp;q=coffee&amp;amp;api_key=YOUR_KEY&amp;amp;output=md" rel="noopener noreferrer"&gt;https://serpapi.com/search?engine=google&amp;amp;q=coffee&amp;amp;api_key=YOUR_KEY&amp;amp;output=md&lt;/a&gt;"&lt;br&gt;
curl "&lt;a href="https://serpapi.com/search.md?engine=google&amp;amp;q=coffee&amp;amp;api_key=YOUR_KEY" rel="noopener noreferrer"&gt;https://serpapi.com/search.md?engine=google&amp;amp;q=coffee&amp;amp;api_key=YOUR_KEY&lt;/a&gt;"&lt;br&gt;
curl -H "Accept: text/markdown" "&lt;a href="https://serpapi.com/search?engine=google&amp;amp;q=coffee&amp;amp;api_key=YOUR_KEY" rel="noopener noreferrer"&gt;https://serpapi.com/search?engine=google&amp;amp;q=coffee&amp;amp;api_key=YOUR_KEY&lt;/a&gt;"&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

&amp;gt; 
&amp;gt; _Ways to get Markdown output_

An application can use different response formats for different tasks. For example, a research agent could consume Markdown for a broad search-and-summarize step, while a separate data collection service uses JSON for structured extraction.

Markdown output works across all 100+ SerpApi APIs — Google, Bing, YouTube, Amazon, Walmart, DuckDuckGo, and every other API we support.

## Conclusion

With SerpApi, you can retrieve search-engine data to support a wide range of applications. Choosing the right response format helps you integrate that data into your workflow with less token consumption and processing time.

**Ready to build?** Explore the [API documentation](https://serpapi.com/) to find the relevant engine, supported parameters, and response fields for your application.

If you have any questions, feel free to reach out to us at [contact@serpapi.com](mailto:contact@serpapi.com).

## Related Posts

- [Introducing SerpApi's New Markdown Output](https://serpapi.com/blog/introducing-serpapi-markdown-output/)

- [Markdown Search Results: SerpApi vs. Exa, Tavily, Firecrawl](https://serpapi.com/blog/markdown-search-results-serpapi-vs-exa-tavily-firecrawl/)

- [How to Fit More Web Search Data into a Local LLM](https://serpapi.com/blog/local-llm-web-search-markdown-output/)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>agents</category>
      <category>api</category>
      <category>json</category>
      <category>llm</category>
    </item>
    <item>
      <title>How to Scrape Amazon Promotions Coupons</title>
      <dc:creator>Noraina Nordin</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:58:58 +0000</pubDate>
      <link>https://dev.to/serpapi/how-to-scrape-amazon-promotions-coupons-3b2i</link>
      <guid>https://dev.to/serpapi/how-to-scrape-amazon-promotions-coupons-3b2i</guid>
      <description>&lt;p&gt;Amazon coupons live on their own promotion pages. Each page lists the discount, the expiry date, how to claim it, the terms, and every product the coupon applies to. That's useful data if you run a deal site, monitor a competitor's discounts, or want to know the real price of a product after the coupon.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiyu1e45x2ax2gujmnrdk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiyu1e45x2ax2gujmnrdk.png" alt="PLACEHOLDER: Amazon promotion page with the promotion ID highlighted in the address bar" width="799" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amazon promotion page&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The problem is that Amazon has no public API endpoint that returns a coupon page's terms and qualifying products. Scraping the page yourself is also tricky because the product grid loads with JavaScript, making a DIY scraper harder to maintain.&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll use the &lt;a href="https://serpapi.com/amazon-promotion-api" rel="noopener noreferrer"&gt;Amazon Promotion API&lt;/a&gt; from SerpApi to retrieve promotion data as clean JSON or Markdown. You'll also export qualifying products to a CSV file and build a small script to check whether your coupons are still active.&lt;/p&gt;

&lt;h2&gt;
  
  
  What data does the Amazon Promotion API return?
&lt;/h2&gt;

&lt;p&gt;Each request takes one promotion ID and returns a &lt;code&gt;promotion_results&lt;/code&gt; object with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;title&lt;/code&gt;:&lt;/strong&gt; the coupon headline, for example &lt;code&gt;Save 25% on your Subscribe &amp;amp; Save subscription&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;saving_info&lt;/code&gt;:&lt;/strong&gt; what the discount is and how it's applied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;schedule&lt;/code&gt;:&lt;/strong&gt; when the promotion runs, such as &lt;code&gt;Limited-time offer&lt;/code&gt; or a date range.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;expiration&lt;/code&gt;:&lt;/strong&gt; the coupon's expiry date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;how_to_claim&lt;/code&gt;:&lt;/strong&gt; the steps a shopper follows to use the coupon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;terms&lt;/code&gt;:&lt;/strong&gt; the terms and conditions, grouped into sections with a &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt;, and &lt;code&gt;items&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;link&lt;/code&gt;:&lt;/strong&gt; the Amazon promotion page URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you add &lt;code&gt;include_products=true&lt;/code&gt;, you also get &lt;code&gt;qualifying_items&lt;/code&gt;, the products the coupon applies to. Each item includes its &lt;code&gt;asin&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;brand&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;extracted_price&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt;, &lt;code&gt;reviews&lt;/code&gt;, &lt;code&gt;prime&lt;/code&gt;, &lt;code&gt;merchant_id&lt;/code&gt;, and a &lt;code&gt;serpapi_amazon_product_link&lt;/code&gt;, which is a ready-made &lt;a href="https://serpapi.com/amazon-product-api" rel="noopener noreferrer"&gt;Amazon Product API&lt;/a&gt; request for that item.&lt;/p&gt;

&lt;p&gt;Here's a trimmed response for promotion &lt;code&gt;ASV27IGU190N&lt;/code&gt; with &lt;code&gt;include_products=true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"promotion_results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"promotion_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ASV27IGU190N"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Save 25% on your Subscribe &amp;amp; Save subscription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"saving_info"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"25% extra savings on the first order, applied at checkout. Coupons may be subject to a maximum dollar value, which will be reflected at checkout if applicable."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schedule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Limited-time offer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"expiration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Coupon Expiry Date January 1, 2027"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"how_to_claim"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Select the Clip Coupon button."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Add 1 item from the products below to your Cart using the Add to Cart button or select See Options for specific product variations."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"terms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How to redeem a coupon:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.amazon.com/promotion/psp/ASV27IGU190N"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"qualifying_items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"asin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B0CDHNX333"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"serpapi_amazon_product_link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://serpapi.com/search.json?amazon_domain=amazon.com&amp;amp;asin=B0CDHNX333&amp;amp;device=desktop&amp;amp;engine=amazon_product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Orgain Organic Kids Dairy 8g Protein Shake, Fruity Cereal, 8.25 oz, 12pk | ..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Orgain"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$19.49"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"extracted_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;19.49&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reviews"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;644&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"prime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"merchant_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ATVPDKIKX0DER"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhcetar4rcmqxd2vpcsad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhcetar4rcmqxd2vpcsad.png" alt="PLACEHOLDER: SerpApi playground showing the Amazon Promotion API response" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SerpApi returns Amazon promotion pages in a structured output&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started with SerpApi
&lt;/h2&gt;

&lt;p&gt;You need a free SerpApi account before you can use the API. You can &lt;a href="https://serpapi.com/pricing" rel="noopener noreferrer"&gt;upgrade to a paid plan&lt;/a&gt; later if you need more searches, faster speeds, or additional features.&lt;/p&gt;

&lt;p&gt;First, &lt;a href="https://serpapi.com/users/sign_up" rel="noopener noreferrer"&gt;create an account&lt;/a&gt; and verify your email. Then grab your API key from &lt;a href="https://serpapi.com/manage-api-key" rel="noopener noreferrer"&gt;your account dashboard&lt;/a&gt; and save it in an environment variable. Set it in your terminal, replacing the placeholder with your real key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_SERPAPI_API_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The free plan includes 250 searches per month, and you can try any promotion live in the &lt;a href="https://serpapi.com/playground?engine=amazon_promotion" rel="noopener noreferrer"&gt;interactive playground&lt;/a&gt; before writing code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔐 You should &lt;a href="https://serpapi.com/blog/how-to-securely-store-api-keys/" rel="noopener noreferrer"&gt;store your API key in a safe location&lt;/a&gt; if you're sharing or publishing your code. If it's ever leaked, you can generate a new one from the dashboard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Install the SerpApi library (optional)
&lt;/h3&gt;

&lt;p&gt;SerpApi has &lt;a href="https://serpapi.com/integrations" rel="noopener noreferrer"&gt;official libraries&lt;/a&gt; for Python, JavaScript, Ruby, Java, and more. They're a thin wrapper around the API and aren't required. The API works just as well with plain GET requests, cURL, or &lt;code&gt;fetch()&lt;/code&gt; in Node.js.&lt;/p&gt;

&lt;h3&gt;
  
  
  Review the Amazon Promotion API documentation
&lt;/h3&gt;

&lt;p&gt;The engine is &lt;code&gt;amazon_promotion&lt;/code&gt;, and the only required parameter is &lt;code&gt;promotion_id&lt;/code&gt;. Other useful parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;include_products&lt;/code&gt;: set to &lt;code&gt;true&lt;/code&gt; to also get the qualifying products. Responses take longer with this on.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;amazon_domain&lt;/code&gt;: the Amazon marketplace to use, such as &lt;code&gt;amazon.co.uk&lt;/code&gt; or &lt;code&gt;amazon.de&lt;/code&gt;. Defaults to &lt;code&gt;amazon.com&lt;/code&gt;. See the &lt;a href="https://serpapi.com/amazon-domains" rel="noopener noreferrer"&gt;supported Amazon domains&lt;/a&gt;. &lt;code&gt;amazon.cn&lt;/code&gt; isn't supported because Amazon no longer serves promotion pages there.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;language&lt;/code&gt;: the page language, such as &lt;code&gt;es_US&lt;/code&gt; on amazon.com. See the &lt;a href="https://serpapi.com/amazon-languages" rel="noopener noreferrer"&gt;supported Amazon languages&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;output&lt;/code&gt;: set to &lt;code&gt;md&lt;/code&gt; for a token-efficient Markdown response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full field reference and live examples, see the &lt;a href="https://serpapi.com/amazon-promotion-api" rel="noopener noreferrer"&gt;Amazon Promotion API documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdk9cy7ewa5h1i0n6zhxm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdk9cy7ewa5h1i0n6zhxm.png" alt="PLACEHOLDER: Amazon Promotion API documentation page" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SerpApi's Amazon Promotion API documentation&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Find the promotion ID
&lt;/h3&gt;

&lt;p&gt;The promotion ID is in the URL of every Amazon coupon page. Open a coupon from a product page (the "Shop items" or "Terms" link next to the coupon badge), and copy the code after &lt;code&gt;/promotion/psp/&lt;/code&gt; or &lt;code&gt;/promotion/details/popup/&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp660jf89rtdnsvetpe6t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp660jf89rtdnsvetpe6t.png" alt="Promotion ID location" width="534" height="83"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Promotion ID location&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to scrape Amazon coupons
&lt;/h2&gt;

&lt;p&gt;The results are same across every library, so use whichever method fits your stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  GET request
&lt;/h3&gt;

&lt;p&gt;This gets the coupon details for a promotion &lt;code&gt;ASV27IGU190N&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://serpapi.com/search.json?engine=amazon_promotion&amp;amp;promotion_id=ASV27IGU190N&amp;amp;api_key=SERPAPI_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, you get JSON, but you can request &lt;strong&gt;Markdown&lt;/strong&gt; by adding &lt;code&gt;output=md&lt;/code&gt; (or using the &lt;code&gt;/search.md&lt;/code&gt; endpoint, or the &lt;code&gt;Accept: text/markdown&lt;/code&gt; header). Markdown is handy when you feed coupon terms to an LLM or AI agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://serpapi.com/search.json?engine=amazon_promotion&amp;amp;promotion_id=ASV27IGU190N&amp;amp;output=md&amp;amp;api_key=SERPAPI_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python
&lt;/h3&gt;

&lt;p&gt;This gets the coupon details and prints the headline, expiry date, and claim steps, using the &lt;a href="https://serpapi.com/integrations/python" rel="noopener noreferrer"&gt;official SerpApi Python library&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;Install the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;serpapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon_promotion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotion_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASV27IGU190N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;promotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotion_results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expiration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;how_to_claim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Save 25% on your Subscribe &amp;amp; Save subscription
Coupon Expiry Date January 1, 2027
- Select the Clip Coupon button.
- Add 1 item from the products below to your Cart using the Add to Cart button or select See Options for specific product variations.
- When you're done shopping, select Go to Cart.
- The offer will automatically be applied at checkout, if eligible.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JavaScript and Node.js
&lt;/h3&gt;

&lt;p&gt;This runs the same request with the &lt;a href="https://serpapi.com/integrations/javascript" rel="noopener noreferrer"&gt;SerpApi JavaScript library&lt;/a&gt;. Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;serpapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a new file &lt;code&gt;index.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getJson&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serpapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJson&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;amazon_promotion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;promotion_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ASV27IGU190N&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;promotion_results&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;how_to_claim&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`- &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  cURL
&lt;/h3&gt;

&lt;p&gt;This gets the coupon details straight from the command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--get&lt;/span&gt; https://serpapi.com/search &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"SERPAPI_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"amazon_promotion"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;promotion_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ASV27IGU190N"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Other languages and no-code solutions
&lt;/h3&gt;

&lt;p&gt;Even if there's no official SerpApi integration for your language, you can use the API directly with GET requests and parse the JSON response. SerpApi also works with no-code tools like &lt;a href="https://serpapi.com/blog/announcing-serpapis-make-app/" rel="noopener noreferrer"&gt;Make.com&lt;/a&gt; and &lt;a href="https://serpapi.com/blog/boost-your-n8n-workflows-with-serpapis-verified-node/" rel="noopener noreferrer"&gt;n8n&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the products that qualify for a coupon
&lt;/h2&gt;

&lt;p&gt;A coupon often covers dozens of products, and only some variants qualify. Add &lt;code&gt;include_products=true&lt;/code&gt; to get the full list. This script reuses the &lt;code&gt;client&lt;/code&gt; from above, prints each product, and saves them to &lt;code&gt;qualifying_items.csv&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;PROMOTION_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASV27IGU190N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon_promotion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotion_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PROMOTION_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualifying_items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;asin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualifying_items.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotion_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="n"&gt;PROMOTION_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Saved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; qualifying items to qualifying_items.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualifying_items_truncated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Note: this list is incomplete. Try again later with no_cache=true.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
   &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This promotion has no product list. It only shows terms.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;B0CDHNX333 | $19.49 | Orgain Organic Kids Dairy 8g Protein Shake, Fruity Cereal, 8
B00BYOW7VG | $21.21 | Orgain Organic Kids Dairy 8g Protein Shake, Vanilla, 8.25 oz,
...
Saved 95 qualifying items to qualifying_items.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhamf5bh98b57eaml0mjy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhamf5bh98b57eaml0mjy.png" alt="Qualifying products exported to CSV" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Qualifying products exported to CSV&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three things to know about this list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not every coupon has a product list.&lt;/strong&gt; Some coupons, often small ones such as "Apply $1 coupon", only have a terms page. For these, the response has no &lt;code&gt;qualifying_items&lt;/code&gt; field, and the &lt;code&gt;title&lt;/code&gt; is a generic &lt;code&gt;View promotion details&lt;/code&gt;. The script prints a note when that happens.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;qualifying_items_truncated&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; when the list is incomplete, either because a very large promotion hit the safety cap or because Amazon didn't return the product grid. The second case can be temporary, so try the request again later with &lt;code&gt;no_cache=true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prices are shelf prices.&lt;/strong&gt; &lt;code&gt;extracted_price&lt;/code&gt; is the price before the coupon. Use &lt;code&gt;saving_info&lt;/code&gt; to calculate the discounted price.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each item also has a &lt;code&gt;serpapi_amazon_product_link&lt;/code&gt;. Call it to get the full product page, including variants, specifications, and reviews. For more on looking up products by ASIN, see our &lt;a href="https://serpapi.com/blog/amazon-asin-lookup-api/" rel="noopener noreferrer"&gt;Amazon ASIN lookup guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Track Amazon coupons with Python
&lt;/h2&gt;

&lt;p&gt;Coupons end early, run out, or get replaced. If you keep a list of promotion IDs, for example from your own brand, your competitors, or a deal feed, this script checks each one and records whether it's still active.&lt;/p&gt;

&lt;p&gt;Here's what the script does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checks every promotion ID in &lt;code&gt;PROMOTION_IDS&lt;/code&gt;, up to &lt;code&gt;MAX_PROMOTIONS&lt;/code&gt; per run&lt;/li&gt;
&lt;li&gt;Records the title, savings, and expiry date for active coupons&lt;/li&gt;
&lt;li&gt;Marks a coupon as &lt;code&gt;ended&lt;/code&gt; when the API says it has ended or wasn't found&lt;/li&gt;
&lt;li&gt;Stops with an error for anything else, such as an invalid API key, so a bad key isn't recorded as an ended coupon&lt;/li&gt;
&lt;li&gt;Saves the results to &lt;code&gt;coupon_status.json&lt;/code&gt; with the date checked
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;PROMOTION_IDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASV27IGU190N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1C1U2DDI9B2GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;MAX_PROMOTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;  &lt;span class="c1"&gt;# each promotion uses one search
&lt;/span&gt;&lt;span class="n"&gt;ENDED_MESSAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Promotion has ended or was not found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_promotion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;promotion_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon_promotion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotion_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promotion_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;http_error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;http_error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ENDED_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotion_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promotion_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ended&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;promotion_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# e.g. invalid API key
&lt;/span&gt;
    &lt;span class="n"&gt;promotion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotion_results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotion_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promotion_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;saving_info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;saving_info&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expiration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expiration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checked_on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;check_promotion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PROMOTION_IDS&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;MAX_PROMOTIONS&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;promotion&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;promotion_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;promotion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coupon_status.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;promotions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Checked &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;promotions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; promotions, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; active. Saved to coupon_status.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ASV27IGU190N | active | Save 25% on your Subscribe &amp;amp; Save subscription
A1C1U2DDI9B2GB | ended | 
Checked 2 promotions, 1 active. Saved to coupon_status.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;code&gt;MAX_PROMOTIONS&lt;/code&gt; caps the number of searches per run. Lower it while testing so you don't use up your monthly searches.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are a few ways you can extend this script:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run it on a schedule.&lt;/strong&gt; Use cron or Task Scheduler to run it daily and compare each run with the last one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on changes.&lt;/strong&gt; Send a message when a coupon ends or its &lt;code&gt;saving_info&lt;/code&gt; changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the products.&lt;/strong&gt; Set &lt;code&gt;include_products&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; to also track which products join or leave a promotion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does Amazon have a coupon API?
&lt;/h3&gt;

&lt;p&gt;Not a public one for coupon pages. Sellers can create and report on their own coupons in Seller Central, and affiliates get product and offer data through Amazon's Product Advertising API. Neither returns a public coupon page with its terms and qualifying products. This Amazon Promotion API fills that gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where do I find an Amazon promotion ID?
&lt;/h3&gt;

&lt;p&gt;In the coupon page URL. It's the code after &lt;code&gt;/promotion/psp/&lt;/code&gt; or &lt;code&gt;/promotion/details/popup/&lt;/code&gt;, for example &lt;code&gt;ASV27IGU190N&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I find every Amazon coupon with this API?
&lt;/h3&gt;

&lt;p&gt;No. It returns one promotion per request, so you need the ID first. To spot products that have a coupon, use the &lt;a href="https://serpapi.com/blog/scrape-amazon-product-data-tutorial/" rel="noopener noreferrer"&gt;Amazon Search API&lt;/a&gt;. Its organic results include a &lt;code&gt;save_with_coupon&lt;/code&gt; field, such as &lt;code&gt;Save 10% with coupon&lt;/code&gt;, but not the promotion ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens when a coupon ends?
&lt;/h3&gt;

&lt;p&gt;The API returns an error: &lt;code&gt;Promotion has ended or was not found.&lt;/code&gt; The tracking script above records these as &lt;code&gt;ended&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it work on other Amazon marketplaces?
&lt;/h3&gt;

&lt;p&gt;Yes. Set &lt;code&gt;amazon_domain&lt;/code&gt; to the marketplace you need, such as &lt;code&gt;amazon.co.uk&lt;/code&gt; or &lt;code&gt;amazon.co.jp&lt;/code&gt;. Every &lt;a href="https://serpapi.com/amazon-domains" rel="noopener noreferrer"&gt;supported Amazon domain&lt;/a&gt; works except &lt;code&gt;amazon.cn&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://serpapi.com/amazon-promotion-api" rel="noopener noreferrer"&gt;Amazon Promotion API&lt;/a&gt; turns any Amazon coupon page into structured JSON or Markdown, so you can check coupon terms, export qualifying products, and track which coupons are still active without maintaining your own scraper. You can then pair it with the &lt;a href="https://serpapi.com/amazon-product-api" rel="noopener noreferrer"&gt;Amazon Product API&lt;/a&gt; to get full details for each qualifying product, or use it alongside an &lt;a href="https://serpapi.com/blog/automate-amazon-price-tracking-to-see-real-world-inflation/" rel="noopener noreferrer"&gt;Amazon price tracker&lt;/a&gt; to see the real price after a coupon.&lt;/p&gt;

&lt;p&gt;If you need help getting started, &lt;a href="https://serpapi.com/#contact" rel="noopener noreferrer"&gt;contact us&lt;/a&gt;. We're happy to help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/blog/scrape-amazon-product-data-tutorial/" rel="noopener noreferrer"&gt;Scrape Amazon Product Data (complete 2026 tutorial)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/blog/amazon-asin-lookup-api/" rel="noopener noreferrer"&gt;Amazon ASIN Lookup API: Find and Fetch Product Details&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/blog/automate-amazon-price-tracking-to-see-real-world-inflation/" rel="noopener noreferrer"&gt;Automate Amazon Price Tracker using Python to See Real World Inflation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/blog/amazon-keyword-research-python/" rel="noopener noreferrer"&gt;How to Do Amazon Keyword Research with Python&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://serpapi.com/blog/product-market-research-using-amazon-search-api/" rel="noopener noreferrer"&gt;Product Market Research using Amazon Search API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Ecommerce Web Scraping: How to Track Competitor Prices, Products, and Stock</title>
      <dc:creator>Srujana Maddula</dc:creator>
      <pubDate>Thu, 29 Oct 2026 11:01:45 +0000</pubDate>
      <link>https://dev.to/serpapi/ecommerce-web-scraping-how-to-track-competitor-prices-products-and-stock-54nn</link>
      <guid>https://dev.to/serpapi/ecommerce-web-scraping-how-to-track-competitor-prices-products-and-stock-54nn</guid>
      <description>&lt;p&gt;Almost all major e-commerce players already scrape each other’s sites, and they feed that data straight into pricing intelligence, inventory stocking and supply chain optimization. If you don't do&amp;nbsp;it, you’re behind.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll show you how ecommerce web scraping works and which data is worth scraping. Even if you can’t pull entire catalogs, you’ll learn the two signals everyone should collect at a minimum. You’ll also get a working script that scrapes a popular e-commerce site in Python. And if you have another marketplace like Amazon or Walmart in mind, I have a solution for that too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ecommerce data is worth scraping?
&lt;/h2&gt;

&lt;p&gt;You can scrape almost anything from an online store. The question is what’s worth the effort. In this section, the first two give you a rich dataset, and the last two are the minimum every store should be scraping.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Product detail pages&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wtg535vvmkyq31bmkuk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wtg535vvmkyq31bmkuk.png" alt="Scrape product pages" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Scrape product pages&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By scraping single product pages, you get title, brand, category, price, availability, customer reviews, product description, and much more about an item.&lt;/p&gt;

&lt;p&gt;Start with how they sell their best sellers. That means compare the descriptions of their top products with their slow movers. Note which features they lead with and which claims they repeat. Then test those same moves on your own listings.&lt;/p&gt;

&lt;p&gt;The product detail page also carries the customer reviews. Reviews tell you which features buyers care about, what breaks or disappoints them, and whether they felt the price was fair. If the same phrase keeps showing up, like “too expensive for what it is,” a better-priced alternative could win those buyers. If reviews keep praising a product as “great value for the price,” you learn what shoppers consider fair and can position your own products around it.&lt;/p&gt;

&lt;p&gt;And here’s the clever part. You’ll never see a competitor’s real sales numbers. But review counts are a decent stand-in. When a product that usually gets a handful of reviews a week suddenly gets dozens, something has happened: a sales spike, a promotion or a product going viral on TikTok. Spotting it early tells you what’s about to be in demand.&lt;/p&gt;

&lt;p&gt;You’ll also see which category/department each product belongs to. Compare them with your own catalog to fix misclassified products and to spot the categories and products you’re missing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Search results listings&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2j34qq7crelnkq3dx681.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2j34qq7crelnkq3dx681.png" alt="Scrape ecommerce search results" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Scrape ecommerce search results&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When shoppers search for a product on a marketplace, the results page shows what the platform thinks is relevant to their query. Those listings are worth scraping.&lt;/p&gt;

&lt;p&gt;You can collect product titles, images, prices, rankings, and other listing details for a search query. This shows how competitors position their products for different search terms, so you can benchmark your own listings, sharpen your titles and descriptions, and spot gaps in your assortment.&lt;/p&gt;

&lt;p&gt;For example, say Amazon's top results for "cordless drill 20V" include bestseller brands or models that Walmart doesn't have. That could be a buying opportunity: stock those products or consider a comparable alternative under your own brand.&lt;/p&gt;

&lt;p&gt;Search results also tells you how a marketplace interprets a query. If the top results for "air fryer" mostly feature large-capacity models, that's a clue about what shoppers may expect to find for that search. If your site keeps surfacing irrelevant products for the same query, you identified to fix a search-relevance problem.&lt;/p&gt;

&lt;p&gt;The above two broad categories: product pages and search results are the two richest datasets. But if you're a smaller retailer and don't need that much data, or don't have the budget to build a comprehensive competitor dataset yet? Start with the price and stock data fields that are useful to almost every e-commerce business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product prices&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Price is the first thing a shopper weighs before buying, and it’s the most tracked field. In a &lt;a href="https://www.banking.senate.gov/newsroom/majority/brown-demands-answers-on-amazon-and-walmarts-use-of-so-called-dynamic-pricing" rel="noopener noreferrer"&gt;June 2024 letter to Walmart and Amazon&lt;/a&gt;, Senator Sherrod Brown pointed out that some retailers using pricing algorithms change their prices more than 2,000 times a day.&lt;/p&gt;

&lt;p&gt;So scrape their prices regularly, and decide in time whether to match a drop, offer a better deal, or hold your price because your margins matter more. Over time, that price history also shows you how they run promotions, so you can plan your own pricing instead of only reacting to theirs. If you only get to track one thing, track this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stock availability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a competitor runs out of a product people want, those shoppers look somewhere else. If you’re watching, that somewhere else can be you. Restock it, feature it, or sharpen your price while they’re empty, and you catch the demand they couldn’t serve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is Ecommerce web scraping difficult?
&lt;/h2&gt;

&lt;p&gt;Ecommerce sites are some of the most heavily defended sites on the web. Basic defenses like IP rate limiting and User-Agent blocking are common across the web. But ecommerce sites often go further, using advanced browser fingerprints and other signals to spot automated traffic.&lt;/p&gt;

&lt;p&gt;Big retailers rely on bot-protection services like Akamai, DataDome and Cloudflare, which look far beyond your IP. They check whether your connection really looks like Chrome, whether your browser has the quirks a normal one would, and whether you scroll, click and pause like a person. A script that loads page after page at perfect intervals stands out right away.&amp;nbsp;&lt;/p&gt;

&lt;p&gt;And even when you get that far, a CAPTCHA can stop you in your tracks. Sites use these challenges when they suspect a visitor is a bot, sometimes alongside other checks that run in the background.&lt;/p&gt;

&lt;p&gt;So a generic automation script won't help pass such sophisticated defense systems. Even if you try to build the first class scraper yourself, you have to spend on proxies and IPs. To remove that hassle, you can try the below simple API call approach to get any e-commerce data regularly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scraping an Ecommerce website in Python
&lt;/h2&gt;

&lt;p&gt;You can send any public URL to SerpApi's Extract API and get the page back as structured JSON or Markdown. In this example, you’ll start from a Best Buy search results page, find the products on it, and pull each product’s title, price and stock status into clean JSON.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You’ll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.8 or later&lt;/li&gt;
&lt;li&gt;A code editor like VS Code, or a Jupyter notebook&lt;/li&gt;
&lt;li&gt;A SerpApi account, which comes with 250 free searches per month.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Set up SerpApi
&lt;/h3&gt;

&lt;p&gt;Sign up at&lt;a href="https://serpapi.com" rel="noopener noreferrer"&gt;serpapi.com&lt;/a&gt; and copy your API key from the dashboard.&lt;/p&gt;

&lt;p&gt;Install the SerpApi Python client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pip&lt;/span&gt; &lt;span class="n"&gt;install&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then save your API key as an environment variable, so it never ends up in your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;export&lt;/span&gt; &lt;span class="n"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Choose your target site and the data you want
&lt;/h3&gt;

&lt;p&gt;In this case, it is Best Buy’s search results for the search term "samsung galaxy": &lt;a href="https://www.bestbuy.com/site/searchpage.jsp?id=pcat17071&amp;amp;st=samsung%20galaxy" rel="noopener noreferrer"&gt;https://www.bestbuy.com/site/searchpage.jsp?id=pcat17071&amp;amp;st=samsung%20galaxy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The search page shows a grid of products, but the details you want live on each product’s own page. So the script works in two passes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetch the search page and collect the product links.&lt;/li&gt;
&lt;li&gt;Visit each product page and pull out the data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For each product, you’ll collect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title&lt;/li&gt;
&lt;li&gt;Product URL&lt;/li&gt;
&lt;li&gt;Price&lt;/li&gt;
&lt;li&gt;Stock availability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike CSS-selector based ecommerce data scraping, you don’t need to open DevTools and hunt for class names. The Extract API reads the page for you, including the structured product data most big stores embed for Google.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build the Ecommerce web scraper
&lt;/h3&gt;

&lt;p&gt;Create a file called &lt;code&gt;best_buy_scraper.py&lt;/code&gt;and add the below code we discuss in each step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Import the packages and set your target&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urlunsplit&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;


&lt;span class="n"&gt;PRODUCT_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;SEARCH_PAGE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.bestbuy.com/site/searchpage.jsp?id=pcat17071&amp;amp;st=samsung%20galaxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PRODUCT_LIMIT&lt;/code&gt; sets how many products you want back. &lt;code&gt;SEARCH_PAGE_URL&lt;/code&gt; is your search term result page of Best Buy. To track another keyword listings, change st=samsung%20galaxy to any search term.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Filter the product links&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bestbuy_product_links&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;headings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unescape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inline_links&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bestbuy.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.bestbuy.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/product/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlunsplit&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;headings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;links&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what each check does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;parsed.scheme&lt;/code&gt;: drops anything that isn’t a regular web link, like mailto: or javascript: links.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hostname&lt;/code&gt;: keeps only links on bestbuy.com or its subdomains, so ads pointing to other sites are out.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;parsed.path.startswith("/product/")&lt;/code&gt;: keeps only product pages. Every Best Buy product URL follows this pattern.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;urlunsplit(...)&lt;/code&gt; and &lt;code&gt;seen&lt;/code&gt;: strips the #fragment from each URL and skips duplicates. The same product often appears twice on a page, once on its image and once on its title.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;headings&lt;/code&gt;: if a link has no title of its own, the function borrows the title from the nearest heading on the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you adapt this script for another store, this is the function to change. Amazon product URLs, for example, contain &lt;code&gt;/dp/&lt;/code&gt; instead of &lt;code&gt;/product/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Since the Extract API is a generic API for all web pages, you need to parse Best Buy URLs to find product links and then parse the JSON output to extract the required fields.&lt;/p&gt;

&lt;p&gt;However, if you want to scrape major retailers, SerpApi &lt;strong&gt;provides dedicated&lt;/strong&gt; &lt;a href="https://serpapi.com/walmart-search-api" rel="noopener noreferrer"&gt;&lt;strong&gt;Walmart&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://serpapi.com/amazon-search-api" rel="noopener noreferrer"&gt;&lt;strong&gt;Amazon&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;, and&lt;/strong&gt; &lt;a href="https://serpapi.com/ebay-product-api" rel="noopener noreferrer"&gt;&lt;strong&gt;eBay APIs&lt;/strong&gt;&lt;/a&gt;, along with specialized APIs for other platforms. A single call to any of these retail APIs returns structured JSON with fields like price, title, product ID, and much more, making the data much easier to work with.&lt;/p&gt;

&lt;p&gt;SerpApi's 250 free searches per month give you access to all these APIs. Check out these guides to &lt;a href="https://serpapi.com/blog/how-to-scrape-walmart-data/" rel="noopener noreferrer"&gt;&lt;strong&gt;scraping Walmart&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://serpapi.com/blog/scrape-amazon-product-data-tutorial/" rel="noopener noreferrer"&gt;&lt;strong&gt;Amazon data&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Pull out price and stock&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This function takes the Extract API’s response for one product page and returns the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_prices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SerpApi Extract request failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_ld&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;offer&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="n"&gt;page_offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_offers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;current_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_price&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;current_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page_offer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priceCurrency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;page_offer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current_price&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;symbol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;current_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;availability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;page_offer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;availability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;In stock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outofstock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Out of stock&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page_information&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;regular_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;current_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;availability&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;availability&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function looks for the price in two places&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;json_ld&lt;/code&gt;: most large stores embed structured product data in their pages so Google can show prices in search results. The function finds the item marked &lt;code&gt;"@type": "Product"&lt;/code&gt; and reads the price, currency and stock status from its offers. This is the most reliable source, because it’s built for machines to read.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;price_offers&lt;/code&gt;: if the structured data has no price, the function falls back to the prices the Extract API found in the visible page content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Handle failed pages and extract successful product pages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you scrape many pages, a few may fail: a timeout, a removed product, a blocked request. Handle them like below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;error_detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request timed out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;extract_prices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serpapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serpapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Skipping &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;error_detail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;extract_product()&lt;/code&gt; fetches each product page with one Extract API call per product. If anything goes wrong, it prints which URL failed and returns None, so the main loop can skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Run the ecommerce scraper&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run the scraper: &lt;code&gt;python best_buy_scraper.py&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here’s the output:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6jyg9owzbrcqyirg65nn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6jyg9owzbrcqyirg65nn.png" width="799" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can each product's title, url, price, and availability, ready to save to a file, load into a spreadsheet or feed into a price tracker. Run it on a schedule and compare each day’s results with the last, and you have a basic competitor price and stock monitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;p&gt;Ecommerce data scraping is now the cost of staying competitive. Dynamic pricing is a baseline, catalogs adapt to customer demand, and shoppers have plenty of alternatives, so working from stale competitor data costs you sales.&lt;/p&gt;

&lt;p&gt;The hard part isn’t writing the scraper, it’s the infra to maintain. Ecommerce sites are among the hardest to scrape, handling proxies, blocks and parsers that break whenever a page changes cost you engineering overload and resource drains.&lt;/p&gt;

&lt;p&gt;As the code in this guide shows, you send a simple request to &lt;a href="https://serpapi.com/" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt; and get structured data back into your pipeline, with none of that infrastructure to maintain. It includes 250 free searches a month, so try it out on your competitor site.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Scrape Booking.com Hotel Listings and Prices (2026 Tutorial)</title>
      <dc:creator>Noraina Nordin</dc:creator>
      <pubDate>Tue, 27 Oct 2026 02:42:49 +0000</pubDate>
      <link>https://dev.to/serpapi/how-to-scrape-bookingcom-hotel-listings-and-prices-2026-tutorial-1g99</link>
      <guid>https://dev.to/serpapi/how-to-scrape-bookingcom-hotel-listings-and-prices-2026-tutorial-1g99</guid>
      <description>&lt;p&gt;Say you want to track what a three-night stay in New York costs, or compare how hotels and vacation rentals are priced across a city. On Booking.com, you'd type a destination, pick your dates, and scroll. Manually extracting this data is slow and difficult to scale, especially across multiple destinations and date ranges. Replicating this process in code usually requires building and maintaining your own Booking.com scraper: rendering JavaScript pages, getting past anti-bot checks, fixing your parser when the HTML changes, and dealing with results that Booking.com ranks differently for every visitor.&lt;/p&gt;

&lt;p&gt;SerpApi’s &lt;a href="https://serpapi.com/booking-search-api" rel="noopener noreferrer"&gt;Booking.com Search API&lt;/a&gt; handles that for you and returns the results as &lt;strong&gt;structured JSON or Markdown&lt;/strong&gt;. The same way you would do on manual search, you can search for a destination based on your travel dates, number of guests, and preferred currency. You’ll receive a list of properties with prices, review scores, room options, policies, and the filters Booking.com offers for that search. This data can be used to build a price tracker, market research dashboard, or travel assistant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmz7qt4gfbbdhr834bgr3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmz7qt4gfbbdhr834bgr3.png" alt="Booking.com interface" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Booking.com interface&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll make your first request with cURL, Python, and JavaScript, then save the results to CSV, page through more properties, and filter by price and amenities.&lt;/p&gt;

&lt;h2&gt;
  
  
  What data can you retrieve with the Booking.com Search API?
&lt;/h2&gt;

&lt;p&gt;Each search returns a &lt;code&gt;properties&lt;/code&gt; array with the places to stay that Booking.com shows for your destination and dates. The most useful data includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Property details:&lt;/strong&gt; &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt; (such as Hotel or Apartment), and a &lt;code&gt;link&lt;/code&gt; to the listing on Booking.com.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prices:&lt;/strong&gt; &lt;code&gt;price&lt;/code&gt; for the whole stay, &lt;code&gt;price_per_night&lt;/code&gt;, &lt;code&gt;taxes_and_fees&lt;/code&gt; that aren’t included in the price, and the &lt;code&gt;currency&lt;/code&gt; they’re in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviews:&lt;/strong&gt; the review score in &lt;code&gt;rating&lt;/code&gt; (out of 10) and the number of &lt;code&gt;reviews&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; &lt;code&gt;location&lt;/code&gt; (district and city) and &lt;code&gt;gps_coordinates&lt;/code&gt; with latitude and longitude.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policies:&lt;/strong&gt; whether the property offers &lt;code&gt;free_cancellation&lt;/code&gt;, plus the rooms recommended for your guests in &lt;code&gt;rooms&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filters:&lt;/strong&gt; the &lt;code&gt;filters&lt;/code&gt; array lists the filters Booking.com offers for your search, which you can reuse in your next request (more on that below).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prices depend on dates. If you don't set &lt;code&gt;check_in_date&lt;/code&gt; and &lt;code&gt;check_out_date&lt;/code&gt;, Booking.com returns properties without prices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fas9xsesll672ci6vsap8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fas9xsesll672ci6vsap8.png" alt="SerpApi's Booking.com Search API documentation" width="799" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SerpApi's Booking.com Search API documentation&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the full list of fields and parameters, see the &lt;a href="https://serpapi.com/booking-search-api" rel="noopener noreferrer"&gt;Booking.com Search API documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to scrape Booking.com hotel listings
&lt;/h2&gt;

&lt;p&gt;You can call the Booking.com Search API from the playground, a plain GET request, cURL, Python, or JavaScript. Every method returns the same results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it in the playground
&lt;/h3&gt;

&lt;p&gt;The quickest way to explore the API is the &lt;a href="https://serpapi.com/playground?engine=booking" rel="noopener noreferrer"&gt;SerpApi interactive playground&lt;/a&gt;. This example searches New York for a three-night stay:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;engine&lt;/code&gt;: &lt;code&gt;booking&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;q&lt;/code&gt;: &lt;code&gt;New York&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check_in_date&lt;/code&gt;: &lt;code&gt;2026-11-22&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check_out_date&lt;/code&gt;: &lt;code&gt;2026-11-25&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpi5v83t5ms6n55t5w4sd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpi5v83t5ms6n55t5w4sd.png" alt="SerpApi's Booking.com search playground" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SerpApi's Booking.com search playground&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Run the search and look at the &lt;code&gt;properties&lt;/code&gt; array: compare &lt;code&gt;price&lt;/code&gt; with &lt;code&gt;price_per_night&lt;/code&gt; and &lt;code&gt;taxes_and_fees&lt;/code&gt;, open the &lt;code&gt;rooms&lt;/code&gt; of a few properties, and scroll down to &lt;code&gt;filters&lt;/code&gt; to see which neighborhoods, property types, and amenities you can filter by.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Swap in your own destination and future dates as you experiment. The &lt;code&gt;q&lt;/code&gt; parameter accepts a city, region, district, landmark, or airport (such as Paris, Tuscany, Manhattan, or Eiffel Tower). However, Booking.com doesn't return results for most street addresses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Get your API key
&lt;/h3&gt;

&lt;p&gt;To call the API from your own code, &lt;a href="https://serpapi.com/users/sign_up" rel="noopener noreferrer"&gt;register for a SerpApi account&lt;/a&gt; and copy your private API key from the &lt;a href="https://serpapi.com/manage-api-key" rel="noopener noreferrer"&gt;API key page&lt;/a&gt; in your dashboard.&lt;/p&gt;

&lt;p&gt;The examples below read the key from an environment variable. Set it in your terminal, replacing the placeholder with your real key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_SERPAPI_API_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should &lt;a href="https://serpapi.com/blog/how-to-securely-store-api-keys/" rel="noopener noreferrer"&gt;store your API key in a safe location&lt;/a&gt; if you're sharing or publishing your code. If it's ever leaked, you can generate a new one from the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make a GET request
&lt;/h3&gt;

&lt;p&gt;The API endpoint is &lt;code&gt;https://serpapi.com/search&lt;/code&gt; with &lt;code&gt;engine=booking&lt;/code&gt;. Here's the same New York search as a plain GET request. JSON is the default output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://serpapi.com/search?engine=booking&amp;amp;q=New+York&amp;amp;check_in_date=2026-11-22&amp;amp;check_out_date=2026-11-25&amp;amp;api_key=YOUR_SERPAPI_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get Markdown instead, add &lt;code&gt;output=md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://serpapi.com/search?engine=booking&amp;amp;q=New+York&amp;amp;check_in_date=2026-11-22&amp;amp;check_out_date=2026-11-25&amp;amp;api_key=YOUR_SERPAPI_API_KEY&amp;amp;output=md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://serpapi.com/markdown-output" rel="noopener noreferrer"&gt;Markdown output&lt;/a&gt; is a token-efficient format with tables, Markdown links, and YAML frontmatter, which works well when you pass results to an LLM or AI agent. Use JSON when your code needs exact field access, such as reading &lt;code&gt;extracted_price&lt;/code&gt; or &lt;code&gt;gps_coordinates.latitude&lt;/code&gt;. The rest of this tutorial uses JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  cURL implementation
&lt;/h3&gt;

&lt;p&gt;Here's the equivalent request in cURL, using the environment variable you set earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--get&lt;/span&gt; https://serpapi.com/search &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"booking"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;q&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"New+York"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;check_in_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2026-11-22"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;check_out_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2026-11-25"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
 &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_SERPAPI_API_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what each parameter does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;engine=booking&lt;/code&gt; selects the Booking.com Search API.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;q&lt;/code&gt; is the destination to search. It's required.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check_in_date&lt;/code&gt; and &lt;code&gt;check_out_date&lt;/code&gt; set the stay in &lt;code&gt;YYYY-MM-DD&lt;/code&gt; format. They're optional, but if you use them, they must be used together to get prices. The check-out date can be up to 90 nights after check-in.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api_key&lt;/code&gt; is your private SerpApi key. It's required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, the search is for 2 adults in 1 room. You can change that with &lt;code&gt;adults&lt;/code&gt;, &lt;code&gt;children&lt;/code&gt;, &lt;code&gt;children_ages&lt;/code&gt;, and &lt;code&gt;rooms&lt;/code&gt;, all documented in the &lt;a href="https://serpapi.com/booking-search-api" rel="noopener noreferrer"&gt;Booking.com Search API documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Python implementation
&lt;/h3&gt;

&lt;p&gt;We'll use the &lt;code&gt;requests&lt;/code&gt; library. Install it first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file named &lt;code&gt;main.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;SERPAPI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Set the SERPAPI_API_KEY environment variable first.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;New York&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check_in_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-11-22&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check_out_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-11-25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_properties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://serpapi.com/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Only print the error type: the full message can contain the request URL and API key.
&lt;/span&gt;        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Request failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unexpected response (HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;).&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SerpApi error (HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Unknown error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;


&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_properties&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The search succeeded, but no properties were returned.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_information&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Destination: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;destination&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_results&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; properties found)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   Location: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   Review score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n/a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rating_text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reviews&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   Stay price: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n/a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price_per_night&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;n/a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; per night)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   Free cancellation: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;free_cancellation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script prints the destination and the first five properties. A few notes on what you're seeing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;price&lt;/code&gt; is the price of the whole stay. By default, it's in each property's currency, which is returned in &lt;code&gt;currency&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;price_per_night&lt;/code&gt; is Booking.com's average nightly rate. It can exclude charges included in &lt;code&gt;price&lt;/code&gt;, so it isn't necessarily the stay price divided by the number of nights.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rating&lt;/code&gt; is the review score out of 10, and &lt;code&gt;reviews&lt;/code&gt; is the number of reviews behind it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobo1enliompgkeh9939r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fobo1enliompgkeh9939r.png" alt="First five properties found in New York" width="541" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First five properties found in New York&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The script uses &lt;code&gt;.get()&lt;/code&gt; for each field because optional fields such as &lt;code&gt;price&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt;, or &lt;code&gt;deals&lt;/code&gt; aren't returned for every property. It also keeps three outcomes separate: a failed request or API error stops the script with a message, a successful search with no properties says so, and a successful search prints results. If a search fails, SerpApi returns a message in the &lt;code&gt;error&lt;/code&gt; field.&lt;/p&gt;

&lt;h4&gt;
  
  
  Export results to CSV
&lt;/h4&gt;

&lt;p&gt;Property listings fit nicely into rows. To save them, add &lt;code&gt;import csv&lt;/code&gt; to the imports at the top of &lt;code&gt;main.py&lt;/code&gt;, then add this function below &lt;code&gt;fetch_properties&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking_properties.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fieldnames&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;free_cancellation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;room_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;gps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gps_coordinates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
            &lt;span class="n"&gt;rooms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rooms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
            &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;position&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;property_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviews&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;extracted_price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;free_cancellation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;free_cancellation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;gps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="c1"&gt;# Keep only the first recommended room and join deals into one cell.
&lt;/span&gt;                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;room_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rooms&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Saved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; properties to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, add this line at the end of the file, after the &lt;code&gt;for&lt;/code&gt; loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;save_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;python main.py&lt;/code&gt; again to create &lt;code&gt;booking_properties.csv&lt;/code&gt;. The nested &lt;code&gt;gps_coordinates&lt;/code&gt; object becomes two columns, the &lt;code&gt;deals&lt;/code&gt; array is joined into one cell, and only the first room from the &lt;code&gt;rooms&lt;/code&gt; array is kept. Use &lt;code&gt;extracted_price&lt;/code&gt; instead of &lt;code&gt;price&lt;/code&gt; when you want to sort or compare numbers in a spreadsheet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7l0m0vqhbkj01715g9qu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7l0m0vqhbkj01715g9qu.png" alt="Properties found in New York in CSV format" width="799" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Properties found in New York in CSV format&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  JavaScript implementation
&lt;/h3&gt;

&lt;p&gt;For Node.js, we'll use SerpApi's official &lt;a href="https://github.com/serpapi/serpapi-javascript/" rel="noopener noreferrer"&gt;JavaScript library&lt;/a&gt;. Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;serpapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file named &lt;code&gt;main.cjs&lt;/code&gt;. The &lt;code&gt;.cjs&lt;/code&gt; extension tells Node.js to treat it as a CommonJS module, so &lt;code&gt;require&lt;/code&gt; works without extra configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getJson&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serpapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Set the SERPAPI_API_KEY environment variable first.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJson&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;booking&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;New York&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;check_in_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-11-22&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;check_out_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-11-25&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Redact the key in case the error message includes the request URL.&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Request failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replaceAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[REDACTED]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SerpApi error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The search succeeded, but no properties were returned.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search_information&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Destination: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total_results&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; properties found)\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`   Location: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`   Review score: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;n/a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rating_text&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reviews&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; reviews`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`   Stay price: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;n/a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_per_night&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;n/a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; per night)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`   Free cancellation: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;free_cancellation&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node main.cjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the same fields as the Python version. If you’d rather pass results straight to an LLM, use the &lt;code&gt;output=md&lt;/code&gt; GET request shown earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other languages and no-code options
&lt;/h3&gt;

&lt;p&gt;SerpApi has libraries for more programming languages, listed on the &lt;a href="https://serpapi.com/integrations" rel="noopener noreferrer"&gt;integrations page&lt;/a&gt;. Since the Booking.com Search API is a regular &lt;code&gt;GET&lt;/code&gt; request with &lt;code&gt;engine=booking&lt;/code&gt;, any HTTP client also works.&lt;/p&gt;

&lt;p&gt;If you prefer no-code tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://serpapi.com/blog/announcing-serpapis-make-app/" rel="noopener noreferrer"&gt;SerpApi's Make.com app&lt;/a&gt; has native modules for some APIs and a Universal "Make an API Call" module where you set the request path and each parameter yourself. Pagination isn't automated in the Universal module.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://serpapi.com/blog/boost-your-n8n-workflows-with-serpapis-verified-node/" rel="noopener noreferrer"&gt;SerpApi's verified n8n node&lt;/a&gt; launched with 22 popular APIs. For APIs that aren't in the node, the announcement shows how to use n8n's HTTP Request node with the &lt;code&gt;engine&lt;/code&gt; parameter, for example &lt;code&gt;engine=booking&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to paginate the results
&lt;/h2&gt;

&lt;p&gt;A search returns 25 properties by default. Here's how to get more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;offset&lt;/code&gt; skips a number of results. With the default page size, &lt;code&gt;offset=25&lt;/code&gt; is page 2 and &lt;code&gt;offset=50&lt;/code&gt; is page 3. It can go up to 1,000; beyond that, Booking.com returns the same properties.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;limit&lt;/code&gt; sets the number of properties per page, from 1 to 100. Booking.com sometimes returns a few extra properties, such as sponsored ones.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session_token&lt;/code&gt; keeps the same Booking.com visitor between requests. Booking.com ranks properties differently for each visitor, so without it, later pages can repeat properties from earlier ones. It's returned in &lt;code&gt;serpapi_pagination.session_token&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;serpapi_pagination.next&lt;/code&gt; is a ready-made link to the next page. It already includes the next &lt;code&gt;offset&lt;/code&gt;, the &lt;code&gt;session_token&lt;/code&gt;, and your original parameters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, the second page of the New York search looks like this, where &lt;code&gt;SESSION_TOKEN_FROM_PAGE_1&lt;/code&gt; is the &lt;code&gt;serpapi_pagination.session_token&lt;/code&gt; value from the first response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://serpapi.com/search?engine=booking&amp;amp;q=New+York&amp;amp;check_in_date=2026-11-22&amp;amp;check_out_date=2026-11-25&amp;amp;offset=25&amp;amp;session_token=SESSION_TOKEN_FROM_PAGE_1&amp;amp;api_key=YOUR_SERPAPI_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In code, the simplest approach is to follow the &lt;code&gt;next&lt;/code&gt; link and add your API key to it. This &lt;code&gt;paginate.py&lt;/code&gt; script fetches up to three pages, stops early when there’s no next page or a page comes back empty, and saves all properties to a JSON file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;SERPAPI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Set the SERPAPI_API_KEY environment variable first.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;MAX_PAGES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;  &lt;span class="c1"&gt;# Each page is a separate search request.
&lt;/span&gt;
&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;New York&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check_in_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-11-22&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;check_out_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-11-25&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://serpapi.com/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;all_properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_PAGES&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Request for page &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unexpected response on page &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;).&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SerpApi error on page &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Unknown error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;all_properties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Page &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;next_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;serpapi_pagination&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;next&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;properties&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;next_link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="c1"&gt;# The next link already carries offset, session_token, and the original filters.
&lt;/span&gt;    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_link&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SERPAPI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booking_properties.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_properties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Saved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_properties&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; properties to booking_properties.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;python paginate.py&lt;/code&gt;. Each page is a separate search and can use a search credit, so keep &lt;code&gt;MAX_PAGES&lt;/code&gt; small while you're testing. If you need more properties per request, raise &lt;code&gt;limit&lt;/code&gt; (up to 100) instead of fetching many small pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customize or filter the results
&lt;/h2&gt;

&lt;p&gt;The Booking.com Search API supports the sorting and filters you'd use on Booking.com itself. Here are three useful ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sort_by&lt;/code&gt;:&lt;/strong&gt; Changes the order of results. The default is &lt;code&gt;popularity&lt;/code&gt; ("Our top picks"). Other options include &lt;code&gt;price&lt;/code&gt; (lowest first), &lt;code&gt;bayesian_review_score&lt;/code&gt; (top reviewed), &lt;code&gt;review_score_and_price&lt;/code&gt;, and &lt;code&gt;distance_from_search&lt;/code&gt; (distance from downtown).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;min_price&lt;/code&gt; and &lt;code&gt;max_price&lt;/code&gt;:&lt;/strong&gt; Limit the price per night, in the currency of the results. They must be used together with a &lt;code&gt;currency&lt;/code&gt; code, such as &lt;code&gt;USD&lt;/code&gt; or &lt;code&gt;EUR&lt;/code&gt;. See the &lt;a href="https://serpapi.com/booking-currencies" rel="noopener noreferrer"&gt;Booking.com currencies page&lt;/a&gt; for supported codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nflt&lt;/code&gt;:&lt;/strong&gt; Applies any other Booking.com filter, such as amenities, meals, neighborhoods, or brands. It's a semicolon-separated list of filter values. You don't need to guess them: take the &lt;code&gt;value&lt;/code&gt; from &lt;code&gt;filters[].options&lt;/code&gt; in a previous response. For example, the documentation uses &lt;code&gt;di=1127&lt;/code&gt; for London's Theatreland neighborhood.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a New York search for stays between 100 and 300 USD per night, sorted by lowest price, limited to properties with a filter value taken from the &lt;code&gt;filters&lt;/code&gt; array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://serpapi.com/search?engine=booking&amp;amp;q=New+York&amp;amp;check_in_date=2026-11-22&amp;amp;check_out_date=2026-11-25&amp;amp;currency=USD&amp;amp;min_price=100&amp;amp;max_price=300&amp;amp;sort_by=price&amp;amp;nflt=roomfacility%3D38&amp;amp;api_key=YOUR_SERPAPI_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example, &lt;code&gt;roomfacility=38&lt;/code&gt; is the "Private bathroom" option from the documentation's New York sample. Note that &lt;code&gt;nflt&lt;/code&gt; values need URL encoding (&lt;code&gt;=&lt;/code&gt; becomes &lt;code&gt;%3D&lt;/code&gt; and &lt;code&gt;;&lt;/code&gt; becomes &lt;code&gt;%3B&lt;/code&gt;); &lt;code&gt;requests&lt;/code&gt; and the JavaScript library handle that for you.&lt;/p&gt;

&lt;p&gt;Other handy filters include &lt;code&gt;free_cancellation=true&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt; for a minimum review score (&lt;code&gt;9&lt;/code&gt;, &lt;code&gt;8&lt;/code&gt;, &lt;code&gt;7&lt;/code&gt;, or &lt;code&gt;6&lt;/code&gt;), &lt;code&gt;hotel_class&lt;/code&gt; for star ratings (for example, &lt;code&gt;4,5&lt;/code&gt;), and &lt;code&gt;property_types&lt;/code&gt; (for example, &lt;code&gt;201,220&lt;/code&gt; for apartments and vacation homes). You can also localize results with &lt;code&gt;hl&lt;/code&gt; for the language and &lt;code&gt;currency&lt;/code&gt; for prices, or set &lt;code&gt;device=mobile&lt;/code&gt; to get Booking.com's mobile results, which can include mobile-only prices. See the &lt;a href="https://serpapi.com/booking-search-api" rel="noopener noreferrer"&gt;Booking.com Search API documentation&lt;/a&gt; for the full list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You can now scrape Booking.com hotel listings for any destination and dates, read their prices, review scores, rooms, and policies, save them to CSV, page through more results with &lt;code&gt;session_token&lt;/code&gt;, and narrow searches with sorting and filters. You can also switch to Markdown output with &lt;code&gt;output=md&lt;/code&gt; when the results are going to an LLM.&lt;/p&gt;

&lt;p&gt;To compare Booking.com prices with other sources, see how to &lt;a href="https://serpapi.com/blog/scraping-google-hotels/" rel="noopener noreferrer"&gt;scrape Google Hotels data&lt;/a&gt;, &lt;a href="https://serpapi.com/blog/how-to-scrape-airbnb-data/" rel="noopener noreferrer"&gt;scrape Airbnb data&lt;/a&gt;, and &lt;a href="https://serpapi.com/blog/how-to-scrape-tripadvisor-tutorial/" rel="noopener noreferrer"&gt;scrape Tripadvisor&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which parameters are required?
&lt;/h3&gt;

&lt;p&gt;Only &lt;code&gt;engine=booking&lt;/code&gt;, &lt;code&gt;q&lt;/code&gt; (the destination), and &lt;code&gt;api_key&lt;/code&gt;. Everything else is optional, but you'll usually want &lt;code&gt;check_in_date&lt;/code&gt; and &lt;code&gt;check_out_date&lt;/code&gt;, because Booking.com returns properties without prices when dates aren't set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why are prices in different currencies?
&lt;/h3&gt;

&lt;p&gt;By default, &lt;code&gt;currency&lt;/code&gt; is &lt;code&gt;hotel_currency&lt;/code&gt;, so each property's price is in its own currency (for example, EUR in Paris). Set &lt;code&gt;currency&lt;/code&gt; to a code such as &lt;code&gt;USD&lt;/code&gt; to get all prices in one currency. The &lt;code&gt;currency&lt;/code&gt; field on each property tells you which one was used.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I search flexible dates?
&lt;/h3&gt;

&lt;p&gt;Yes. Set &lt;code&gt;flex_window&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt;, or &lt;code&gt;7&lt;/code&gt; to let the check-in and check-out dates move up to that many days earlier or later for the best price of each property. Each property then includes the &lt;code&gt;check_in_date&lt;/code&gt; and &lt;code&gt;check_out_date&lt;/code&gt; of its price. &lt;code&gt;flex_window&lt;/code&gt; must be used with both dates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is this the official Booking.com API?
&lt;/h3&gt;

&lt;p&gt;No. The Booking.com Search API is a SerpApi API that returns Booking.com's public search results (the same properties, prices, and filters you see on Booking.com) as structured data. It's a separate product from the APIs Booking.com offers its own partners, and you only need a SerpApi API key to use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Booking.com Search API support Markdown output?
&lt;/h3&gt;

&lt;p&gt;Yes. Add &lt;code&gt;output=md&lt;/code&gt; to get Markdown, which is useful for LLMs and AI agents. You can also use the &lt;code&gt;/search.md&lt;/code&gt; endpoint or the &lt;code&gt;Accept: text/markdown&lt;/code&gt; request header. See the &lt;a href="https://serpapi.com/markdown-output" rel="noopener noreferrer"&gt;Markdown output page&lt;/a&gt; for details.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>From JSON to Markdown: Reducing AI SEO Analysis Token Costs by 90%</title>
      <dc:creator>Magenta Qin</dc:creator>
      <pubDate>Sun, 25 Oct 2026 10:33:25 +0000</pubDate>
      <link>https://dev.to/serpapi/from-json-to-markdown-reducing-ai-seo-analysis-token-costs-by-90-3bj9</link>
      <guid>https://dev.to/serpapi/from-json-to-markdown-reducing-ai-seo-analysis-token-costs-by-90-3bj9</guid>
      <description>&lt;p&gt;AI SEO analysis pipelines usually run into two problems quickly.&lt;/p&gt;

&lt;p&gt;First, the more data you analyze, the more input tokens you send to the model. Adding more keywords, markets, competitors, pages, or result rows makes every prompt longer. That may not matter for a one-off analysis, but it matters when a pipeline runs thousands of times a day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7nceofwnt2a8i09dml3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7nceofwnt2a8i09dml3.png" width="799" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Second, cutting the input down can make the analysis less useful. When the full dataset does not fit into the model context, you might often split it into batches, summarize each batch, or rely on agent memory. Those techniques help, but they can not replace the original data. If positions 1-10 and positions 21-30 land in different batches, the model may never compare them side by side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbzimola1psgez2eka7vs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbzimola1psgez2eka7vs.png" width="800" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core question is simple: Can we help the model see more SEO evidence in the same token budget?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this post, I will compare JSON and Markdown as API response formats. I will first reproduce a token benchmark for Google Shopping results. Then I will run a practical SEO analysis demo to see whether the larger sample changes the model's recommendation. The demo code is available &lt;a href="https://github.com/serpapi/serpapi-md-demo" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why build an AI SEO analysis pipeline?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Existing SEO tools already provide rich datasets, dashboards, reports, and AI features. For many workflows, that is enough.&lt;/p&gt;

&lt;p&gt;But internal SEO questions often depend on business context that a general-purpose SEO platform does not have. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why did a competitor's product enter the top results for this query?&lt;/li&gt;
&lt;li&gt;Which ranking changes should the team handle today?&lt;/li&gt;
&lt;li&gt;Does this search result change matter for revenue, conversion rate, inventory, or a product launch?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you build your own AI SEO analysis pipeline, you can choose the market, device, timestamp, query set, and data source. You can also combine search data with internal business data before asking the model to reason over it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal is not to rebuild a full SEO platform. The goal is to add a decision layer that matches your company's workflow.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The hidden cost of API response format&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Most APIs return JSON by default. JSON is excellent for code. It gives programs exact fields, predictable structure, and a reliable contract. But when the next consumer is an AI model, the response format also affects context usage. Consider a simplified Google Shopping result in JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shopping_results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Trail Runner X"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$89"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.7&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Aero Sprint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$74"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same information can be represented as a Markdown table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Shopping Results&lt;/span&gt;

| Position | Title | Price | Rating |
|---:|---|---:|---:|
| 1 | Trail Runner X | $89 | 4.7 |
| 2 | Aero Sprint | $74 | 4.5 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON response repeats keys such as &lt;code&gt;position&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;price&lt;/code&gt;, and &lt;code&gt;rating&lt;/code&gt; for every item. It also includes quotes, commas, braces, and nesting. The Markdown table keeps the field names in the header and uses rows for the values. Both formats describe the same result set, but Markdown often uses fewer tokens. &lt;strong&gt;That means the same model context can fit more search results before batching becomes necessary.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is why Markdown is moving beyond documentation. In April 2025, Cloudflare added a Browser Rendering API endpoint that converts webpages into Markdown. In February 2026, Notion documents page content access as Markdown in its data APIs. In August 2026, SerpApi supports Markdown output for search results by using &lt;code&gt;output=md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft37bxjfreo08rpgqmuuo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft37bxjfreo08rpgqmuuo.png" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The shared pattern is clear: API responses are no longer consumed only by programs. They may go directly into an AI agent's context.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Benchmark: How many input tokens can Markdown save?
&lt;/h2&gt;

&lt;p&gt;SerpApi published a &lt;a href="https://serpapi.com/markdown-output" rel="noopener noreferrer"&gt;Markdown output benchmark&lt;/a&gt; comparing JSON and Markdown responses across several APIs. The result depends on the API, query, returned fields, and tokenizer, but the largest savings appear in more complex result sets.&lt;br&gt;&lt;br&gt;
In that benchmark:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo9tr9cdxpbrodbivs7g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feo9tr9cdxpbrodbivs7g.png" width="800" height="495"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These numbers compare input tokens only. If a model provider charges the same price per input token, reducing input tokens reduces that part of the cost. The total cost of a full AI pipeline will still depend on model choice, output length, orchestration, retries, caching, and other API calls. I wanted to reproduce the Google Shopping result with my own code.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Reproducing the Google Shopping token benchmark&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The reproduction changes only one variable: response format. I prepared 12 Google Shopping queries. For each query, I used the same location, language, and device settings. Then I requested the same Google Shopping results twice through SerpApi: once as JSON and once as Markdown.&lt;/p&gt;

&lt;p&gt;I sent the raw API responses to the same DeepSeek model and read &lt;code&gt;usage.prompt_tokens&lt;/code&gt; from the DeepSeek API response. That value reports the actual number of input tokens the model received. I disabled thinking mode because this experiment measures input representation, not reasoning quality. I did not ask DeepSeek to solve an SEO problem in this benchmark. I used it as the token accounting endpoint.&lt;/p&gt;

&lt;p&gt;The calculation was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;saving &lt;span class="o"&gt;=&lt;/span&gt; 100 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;1 - markdown_tokens / json_tokens&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's execute it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;0:00&lt;br&gt;
/0:18&lt;br&gt;
1×&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Across the 12 queries, Markdown reduced input tokens by an average of &lt;strong&gt;89.55%&lt;/strong&gt;. Individual queries ranged from &lt;strong&gt;86.59%&lt;/strong&gt; to &lt;strong&gt;91.20%&lt;/strong&gt;. That closely matches the published Google Shopping benchmark of about 90%.&lt;/p&gt;

&lt;p&gt;The run saves intermediate evidence so the result can be checked later:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;raw/*.json&lt;/code&gt; and &lt;code&gt;raw/*.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Original API responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deepseek-usage/*.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;DeepSeek token usage receipts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;results.csv&lt;/code&gt; and &lt;code&gt;results.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Per-query comparison data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;summary.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Final summary table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For this sample, Markdown compressed the Google Shopping inputs to roughly one-tenth of the JSON token usage.&lt;/p&gt;

&lt;p&gt;The next question is more important for AI SEO: if Markdown lets the model see more results in the same budget, can that change the analysis?&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A practical SEO demo: Trail running shoes&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For the second experiment, I used a practical Google Shopping question: I sell trail running shoes. To show up higher in Google Shopping, should I optimize titles, lower prices, or get more reviews?&lt;/p&gt;

&lt;p&gt;I fixed the search setup:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Query&lt;/td&gt;
&lt;td&gt;&lt;code&gt;trail running shoes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Market&lt;/td&gt;
&lt;td&gt;New York, United States&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;English&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fields kept&lt;/td&gt;
&lt;td&gt;position, title, price, rating, reviews, source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input budget&lt;/td&gt;
&lt;td&gt;1,550 prompt tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Same DeepSeek model and prompt for both formats&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SerpApi returned the same Google Shopping snapshot with 30 products. Python then measured how many products could fit into the 1550-token budget for each format.&lt;/p&gt;

&lt;p&gt;Let's run it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;0:00&lt;br&gt;
/0:15&lt;br&gt;
1×&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Products that fit&lt;/th&gt;
&lt;th&gt;Prompt tokens used&lt;/th&gt;
&lt;th&gt;Position coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;1,548&lt;/td&gt;
&lt;td&gt;1-16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;1,515&lt;/td&gt;
&lt;td&gt;1-30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the same token budget, Markdown let DeepSeek see &lt;strong&gt;1.88x more products&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;The larger sample changed the recommendation&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The JSON sample only covered positions 1-16. In that sample, the average query-term coverage in product titles was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample&lt;/th&gt;
&lt;th&gt;Query-term coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Top positions 1-5&lt;/td&gt;
&lt;td&gt;27%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lower positions 12-16&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That difference was small. DeepSeek treated the title signal as weak and did not see titles as a clear explanation for position.&lt;br&gt;&lt;br&gt;
The Markdown sample covered all 30 products. With the full sample, the title pattern became clearer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample&lt;/th&gt;
&lt;th&gt;Query-term coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Top positions 1-10&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lower positions 21-30&lt;/td&gt;
&lt;td&gt;17%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the additional 14 products, DeepSeek gave a more specific recommendation: test title changes for lower-position products by adding terms such as "trail running" and gender-specific modifiers. Price and review count did not show meaningful rank relationships in this snapshot:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;JSON sample&lt;/th&gt;
&lt;th&gt;Markdown sample&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spearman position vs. price&lt;/td&gt;
&lt;td&gt;0.001&lt;/td&gt;
&lt;td&gt;0.155&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spearman position vs. reviews&lt;/td&gt;
&lt;td&gt;0.118&lt;/td&gt;
&lt;td&gt;-0.031&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the supported next step was not to cut prices or chase more reviews for ranking. The better next step was to run a title A/B test.&lt;/p&gt;

&lt;p&gt;This does not prove that title changes cause higher Google Shopping positions. A single search snapshot can only generate hypotheses. But it does show how a compact response format can expose a signal that a smaller sample hides.&lt;/p&gt;

&lt;p&gt;Markdown did not make the model smarter. It gave the model more original evidence to compare in the same context budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;JSON and Markdown both have a role&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is not a case for replacing JSON everywhere. JSON remains the better format when code needs a strict contract: tool arguments, IDs, status fields, saved state, validation, and orchestration logic. Markdown is useful when the next step is model understanding. If an AI agent needs to read, compare, summarize, or reason over search results, Markdown can preserve a clear structure while reducing token overhead.&lt;/p&gt;

&lt;p&gt;A practical agent workflow can use both:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Better fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Control data&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool arguments&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IDs and state&lt;/td&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search result context for the model&lt;/td&gt;
&lt;td&gt;Markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documents, citations, and comparison tables&lt;/td&gt;
&lt;td&gt;Markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What I learned&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This experiment led to three takeaways.&lt;/p&gt;

&lt;p&gt;First, building an AI SEO pipeline gives teams control over what the model sees and how recommendations get tested. The value comes from connecting search data with the business question, not from rebuilding a full SEO platform.&lt;/p&gt;

&lt;p&gt;Second, Markdown can reduce input token usage substantially for search result payloads. In our Google Shopping benchmark reproduction, Markdown reduced input tokens by &lt;strong&gt;89.55%&lt;/strong&gt; on average.&lt;/p&gt;

&lt;p&gt;Third, a more compact input format can change the model's answer by increasing evidence coverage. In the Shopping demo, JSON fit 16 products while Markdown fit all 30. The larger sample changed the title signal and led DeepSeek to recommend testing title relevance first.&lt;/p&gt;

&lt;p&gt;If you are building an AI agent or AI SEO pipeline, test both formats in one real workflow. Compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Actual input tokens reported by the model API&lt;/li&gt;
&lt;li&gt;How much source data fits into the prompt&lt;/li&gt;
&lt;li&gt;Whether the larger context changes the final recommendation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;File size alone is not enough. What matters is how much useful evidence the model can see when it makes the decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Try Markdown output with SerpApi&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;SerpApi supports Markdown responses for search APIs with &lt;code&gt;output=md&lt;/code&gt;. If your workflow sends search results into an LLM or AI agent, this can reduce formatting overhead and help the model analyze more results in one context.&lt;br&gt;&lt;br&gt;
You can try SerpApi with a free account, which includes 250 searches per month. If you have questions or feedback about Markdown responses, contact us at &lt;a href="mailto:contact@serpapi.com"&gt;contact@serpapi.com&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  AI #SEO #markdown
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>performance</category>
      <category>seo</category>
    </item>
    <item>
      <title>RubyLLM 2.x: What's new for your agents</title>
      <dc:creator>Josef Strzibny</dc:creator>
      <pubDate>Sat, 24 Oct 2026 10:24:10 +0000</pubDate>
      <link>https://dev.to/serpapi/rubyllm-2x-whats-new-for-your-agents-9fp</link>
      <guid>https://dev.to/serpapi/rubyllm-2x-whats-new-for-your-agents-9fp</guid>
      <description>&lt;p&gt;RubyLLM 2.0 and 2.1 are out and come with new APIs for video, speech, OCR, reranking, batches, conversation controls, expanded list of supported providers and even better Rails integration. But that's not all. If you build chat-based agents in Rails, tools can now wait for a human to approve them, you can run the agent loop one step at a time, and the agents can live in the database and survive a restart. So let's have a look on how to upgrade our old agents and give them new capabilities.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 This post is mainly about RubyLLM 2.0 and 2.1 features for agents. For the full list, see also the official &lt;a href="https://rubyllm.com/whats-new-in-2-0/" rel="noopener noreferrer"&gt;What's New in 2.0&lt;/a&gt; and &lt;a href="https://rubyllm.com/whats-new-in-2-1/" rel="noopener noreferrer"&gt;2.1&lt;/a&gt; guides.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A quick look on the changes
&lt;/h2&gt;

&lt;p&gt;The short version of both releases include the following main additions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool approvals.&lt;/strong&gt; Mark a tool with &lt;code&gt;requires_approval&lt;/code&gt; and the agent pauses until a human says yes or no.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caller-controlled loop.&lt;/strong&gt; &lt;code&gt;ask_later&lt;/code&gt;, &lt;code&gt;generate&lt;/code&gt;, &lt;code&gt;run_tools&lt;/code&gt;, and &lt;code&gt;step&lt;/code&gt; let you run the agent loop one piece at a time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durable agents.&lt;/strong&gt; Agents can be backed by your &lt;code&gt;Chat&lt;/code&gt; model and resumed across requests, jobs, and restarts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider-hosted tools.&lt;/strong&gt; Web search, code execution, and remote MCP servers through one &lt;code&gt;provider_tools&lt;/code&gt; API, with citations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batches.&lt;/strong&gt; Submit many chats at once at the provider's batch rates and collect results later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallbacks, prompt caching, and cost tracking.&lt;/strong&gt; &lt;code&gt;with_fallbacks&lt;/code&gt;, &lt;code&gt;with_caching&lt;/code&gt;, and normalized &lt;code&gt;tokens&lt;/code&gt; and &lt;code&gt;cost&lt;/code&gt; on every response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP client.&lt;/strong&gt; Connect agents to Linear, GitHub, Notion, or any MCP server with a class, the same way you describe a tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool progress.&lt;/strong&gt; Tools report what they're doing while they run, and your UI can show it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool search.&lt;/strong&gt; Mark tools as deferred and the model searches for the ones it needs instead of receiving every definition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluations.&lt;/strong&gt; Run your agent against a dataset of reference answers and get a graded report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache lifetimes per boundary.&lt;/strong&gt; &lt;code&gt;cache_until_here(ttl: "1h")&lt;/code&gt; for the prefix that never changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster by default.&lt;/strong&gt; Streaming parses incrementally, replies stop retaining requests, and connections can be reused.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  New agent capabilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tool approvals
&lt;/h3&gt;

&lt;p&gt;One of the biggest additions to RubyLLM is the ability to approve tool calls. The obvious problem is that nobody wants a language model repricing the catalog on its own. RubyLLM 2.0 solves this with &lt;code&gt;requires_approval&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpdatePrice&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Tool&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s2"&gt;"Updates the price of a product in our catalog"&lt;/span&gt;
  &lt;span class="n"&gt;requires_approval&lt;/span&gt;

  &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;desc: &lt;/span&gt;&lt;span class="s2"&gt;"Product SKU"&lt;/span&gt;
  &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;desc: &lt;/span&gt;&lt;span class="s2"&gt;"New price in USD"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;sku: &lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;price: &lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="s2"&gt;"Updated &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; to $&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to the agent's tools and ask for a change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PriceMonitorAgent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Agent&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="no"&gt;LookupProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SearchGoogleShopping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UpdatePrice&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PriceMonitorAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt; &lt;span class="s2"&gt;"Lower the Chemex 6-Cup to match the market average."&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;awaiting_approval?&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent still runs &lt;code&gt;LookupProduct&lt;/code&gt; and &lt;code&gt;SearchGoogleShopping&lt;/code&gt; on its own. When it gets to &lt;code&gt;UpdatePrice&lt;/code&gt;, it stops. The tool call stays pending and &lt;code&gt;ask&lt;/code&gt; returns. You can inspect what the model wants to do before anything touches the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pending_approvals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="c1"&gt;# update_price {"sku"=&amp;gt;"CHX-6", "price"=&amp;gt;44.97}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then approve or deny and let the agent finish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pending_approvals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;deny&lt;/code&gt; works the same way. The model gets told its request was rejected and can respond accordingly, usually by explaining what it would have done.&lt;/p&gt;

&lt;p&gt;This is the single most useful change in 2.0 for anyone running agents against production data. Read-only tools backed by SerpApi can run freely. Tools that adjust live prices might need a human in the loop, and now that's possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controlling the agent loop
&lt;/h3&gt;

&lt;p&gt;In RubyLLM 1.x, &lt;code&gt;ask&lt;/code&gt; ran the whole thing: send the message, run tools, send results back, repeat until the model answers. That's convenient until you want to be more specific and choose the number of rounds, log every step, or run each turn in a separate background job.&lt;/p&gt;

&lt;p&gt;RubyLLM 2.0 breaks the loop into 4 parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ask_later&lt;/code&gt; stages the question without sending it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;generate&lt;/code&gt; asks the model for one response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run_tools&lt;/code&gt; executes any pending tool calls.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;step&lt;/code&gt; advances by one generation or one tool execution, whichever is next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the previous loop can be run one step at the time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PriceMonitorAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask_later&lt;/span&gt; &lt;span class="s2"&gt;"Which of our coffee products are priced more than 15% above market?"&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;  &lt;span class="c1"&gt;# model decides to call LookupProduct and SearchGoogleShopping&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_tools&lt;/span&gt; &lt;span class="c1"&gt;# the SerpApi searches happen here&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;  &lt;span class="c1"&gt;# model reads the results and answers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing new happens compared to &lt;code&gt;ask&lt;/code&gt;, but you can now put a counter, a timeout, or a job boundary between any two lines. For a pricing agent that might run dozens of Google Shopping searches, being able to say "stop after ten tool rounds" is a feature, not a limitation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rails durable agents
&lt;/h3&gt;

&lt;p&gt;Previously we could persist chats with &lt;code&gt;acts_as_chat&lt;/code&gt;. Now we can go further and lets the agent itself be the record. Point the agent at your chat model with &lt;code&gt;chat_model&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PriceMonitorAgent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Agent&lt;/span&gt;
  &lt;span class="n"&gt;chat_model&lt;/span&gt; &lt;span class="no"&gt;Chat&lt;/span&gt;
  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="s2"&gt;"gpt-5.4-mini"&lt;/span&gt;

  &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;PROMPT&lt;/span&gt;&lt;span class="sh"&gt;
    You are a pricing analyst for the merchandising team.
    Always show specific numbers when comparing prices.
&lt;/span&gt;&lt;span class="no"&gt;  PROMPT&lt;/span&gt;

  &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="no"&gt;LookupProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SearchGoogleShopping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UpdatePrice&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use &lt;code&gt;create!&lt;/code&gt; instead of &lt;code&gt;new&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PriceMonitorAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt; &lt;span class="s2"&gt;"Lower the Chemex 6-Cup to match the market average."&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;awaiting_approval?&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where approvals get practical. The request that triggered the agent has long returned. The merchandiser sees the proposed price change in the UI, maybe hours later, and clicks approve. Your controller or job just reloads the agent and continues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PriceMonitorAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:chat_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:tool_call_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transcript, the pending tool call, and the decision all live in your database. If a job dies halfway, you can resume from the last saved step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 One caveat from the docs: an interrupted operation may run again, so tools should tolerate retries. &lt;code&gt;UpdatePrice&lt;/code&gt; is fine since setting a price twice is harmless. A tool that sends an email should check first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Your app still owns the &lt;code&gt;chats&lt;/code&gt; and &lt;code&gt;messages&lt;/code&gt; tables. RubyLLM 2.0 takes ownership of the supporting tables for the model registry, tool calls, usage, and batches, so it can evolve them without you maintaining the models.&lt;/p&gt;

&lt;h3&gt;
  
  
  Provider tools
&lt;/h3&gt;

&lt;p&gt;The next useful addition is a shared API for tools hosted by the provider. Things like web search, code execution, and remote MCP servers. You enable them with &lt;code&gt;with_provider_tools&lt;/code&gt; on a chat or &lt;code&gt;provider_tools&lt;/code&gt; on an agent, and the results come back with citations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PriceMonitorAgent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Agent&lt;/span&gt;
  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="s2"&gt;"claude-sonnet-5"&lt;/span&gt;
  &lt;span class="n"&gt;provider_tools&lt;/span&gt; &lt;span class="ss"&gt;:web_search&lt;/span&gt;
  &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="no"&gt;LookupProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SearchGoogleShopping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;UpdatePrice&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PriceMonitorAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt; &lt;span class="s2"&gt;"Any recent news about Chemex pricing?"&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;citations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Provider web search is great starting point for open-ended questions. The tool returns answers with links.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider web search vs. SerpApi tools&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Previously, I showed you &lt;a href="https://serpapi.com/blog/how-to-implement-rubyllm-agents/" rel="noopener noreferrer"&gt;how to build RubyLLM agents&lt;/a&gt; with the examples of wrapping SerpApi SDK to give your agents access to 100+ search engines. While it's now easier than ever to just use the default search, a SerpApi-backed tool gives you structured data from a very specific engine.&lt;/p&gt;

&lt;p&gt;For example, SerpApi's &lt;a href="https://serpapi.com/google-shopping-api?" rel="noopener noreferrer"&gt;Google Shopping API&lt;/a&gt; always returns a title, a price, and a source every single time. You pick the engine, so swapping to &lt;a href="https://serpapi.com/amazon-product-api" rel="noopener noreferrer"&gt;Amazon&lt;/a&gt;, Walmart, or eBay is a one-line change. You control the parameters, so you can pin the location or currency. And you can see every query the agent ran in the search inspector.&lt;/p&gt;

&lt;p&gt;This gives you accuracy and flexibility the default search cannot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batch processing
&lt;/h3&gt;

&lt;p&gt;Checking one product price in a chat is one thing. But what about checking the whole catalog every night? RubyLLM now adds batch processing on top of &lt;code&gt;ask_later&lt;/code&gt; which lets you use providers batch rates for more affordable queries.&lt;/p&gt;

&lt;p&gt;An example of this could be submitting a chat question in bulk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;chats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;category: &lt;/span&gt;&lt;span class="s2"&gt;"coffee"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;market&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SerpApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;engine: &lt;/span&gt;&lt;span class="s2"&gt;"google_shopping"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shopping_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"gpt-5.4-mini"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_instructions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"You are a pricing analyst. Answer in one sentence."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Our &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is $&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Market: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. "&lt;/span&gt; &lt;span class="p"&gt;\&lt;/span&gt;
               &lt;span class="s2"&gt;"Are we more than 15% above market?"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save &lt;code&gt;batch.id&lt;/code&gt; somewhere and another background job can check on it later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;provider: :openai&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete?&lt;/span&gt;
  &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Results come back in submission order, so zipping them with the products list is trivial. The &lt;a href="https://rubyllm.com/batches/" rel="noopener noreferrer"&gt;Batches guide&lt;/a&gt; covers failures, embedding batches, and how tool calls behave inside a batch if you'd rather let the model call the tool itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallbacks, caching, and cost
&lt;/h3&gt;

&lt;p&gt;Finally let's go over three smaller (but neat) additions that matter once the agent runs on a schedule rather than in a console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fallbacks.&lt;/strong&gt; When the primary model fails with a transient error, try another one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"gpt-5.4-mini"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LookupProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SearchGoogleShopping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
             &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_fallbacks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The conversation keeps its tools and settings, so pick a fallback that supports what you use. You need credentials for both providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt caching.&lt;/strong&gt; Our agent sends the same instructions on every request. Mark them as a cacheable prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with_caching&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_instructions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"pricing-policy.txt"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;cache_until_here&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt; &lt;span class="s2"&gt;"Is the Aeropress competitively priced?"&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Providers still decide the minimum prefix length and lifetime, and a cache hit isn't guaranteed. But for a long pricing policy that doesn't change, it adds up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost tracking.&lt;/strong&gt; Every response now reports normalized tokens and cost, including the failed attempts behind a fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unknown pricing stays &lt;code&gt;nil&lt;/code&gt; rather than showing as zero. In Rails, usage is recorded in its own ledger, so changing model prices later doesn't rewrite history. If you've ever been asked "how much does this agent cost us per day?", this is the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading from 1.x
&lt;/h2&gt;

&lt;p&gt;Install or upgrade the gem with the following Bundler call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="n"&gt;ruby_llm&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applications on 1.x go to 2.0 first, then 2.1. From 2.1 on, each release ships the upgrade from the release before it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle update ruby_llm &lt;span class="nt"&gt;--version&lt;/span&gt; 2.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 2.0 step is mostly renames, with one name per concept across the library. For tools, &lt;code&gt;param&lt;/code&gt; became &lt;code&gt;parameter&lt;/code&gt;, &lt;code&gt;desc:&lt;/code&gt; became &lt;code&gt;description:&lt;/code&gt;, and &lt;code&gt;params do ... end&lt;/code&gt; became &lt;code&gt;parameters do ... end&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A 1.x tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LookupProduct&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Tool&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s2"&gt;"Looks up a product in our catalog by name"&lt;/span&gt;
  &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;desc: &lt;/span&gt;&lt;span class="s2"&gt;"Product name to search for"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LookupProduct&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Tool&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s2"&gt;"Looks up a product in our catalog by name"&lt;/span&gt;
  &lt;span class="n"&gt;parameter&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Product name to search for"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent class itself needs no changes. Other renames you'll hit first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;max_tokens&lt;/code&gt; is now &lt;code&gt;max_output_tokens&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;with_params&lt;/code&gt; and provider-specific options go through &lt;code&gt;with_provider_options&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;response.input_tokens&lt;/code&gt; and friends live under &lt;code&gt;response.tokens&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Callbacks follow Rails naming: &lt;code&gt;on_tool_call&lt;/code&gt; is &lt;code&gt;before_tool_call&lt;/code&gt;, &lt;code&gt;on_end_message&lt;/code&gt; is &lt;code&gt;after_message&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tools can no longer &lt;code&gt;halt&lt;/code&gt; the loop; use approvals or the caller-controlled loop instead.&lt;/li&gt;
&lt;li&gt;Tool results that return a Hash or Array are sent as JSON text instead of Ruby's &lt;code&gt;inspect&lt;/code&gt; output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Rails, version 2.0 moves the model registry, tool calls, usage, and batches into RubyLLM-owned tables. &lt;code&gt;bin/rails generate ruby_llm:upgrade&lt;/code&gt; creates the migrations in phases, with cleanup in a later deploy, and an optional &lt;code&gt;--mode copy&lt;/code&gt; keeps a route back to 1.16 while you verify. Then run the generator again on 2.1 to add the MCP credentials table and the new columns.&lt;/p&gt;

&lt;p&gt;The official &lt;a href="https://rubyllm.com/upgrading/" rel="noopener noreferrer"&gt;upgrade guide&lt;/a&gt; has the full list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also new
&lt;/h2&gt;

&lt;p&gt;Here are some other things I skipped because they don't change how agents work, but which you might want to start using too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RubyLLM.animate&lt;/code&gt; is now available for requesting video&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RubyLLM.speak&lt;/code&gt; for speech&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RubyLLM.transcribe&lt;/code&gt; for transcription with speaker labels and timestamps&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RubyLLM.ocr&lt;/code&gt; for PDFs and scans&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RubyLLM.rerank&lt;/code&gt; for search results&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RubyLLM.research&lt;/code&gt; for provider-hosted deep research&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RubyLLM.workflow&lt;/code&gt; for grouping steps in your instrumentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus there are now four new providers too: Cohere, Deepgram, ElevenLabs, and Ollama Cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;RubyLLM 2.x makes agents safer to run against real data. Approvals keep humans in charge of writes, loop control lets you bound what an agent does, and durable agents mean a pending decision can wait in your database until someone makes it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://serpapi.com" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt; fits in the same place it did before: as &lt;a href="https://serpapi.com/blog/how-to-implement-rubyllm-agents/" rel="noopener noreferrer"&gt;an agent tool&lt;/a&gt; that gives your agent structured, inspectable search data from Google Shopping, Amazon, Walmart, and other engines. Give it a try with 250 free searches a month.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>ruby</category>
    </item>
    <item>
      <title>How to Fit More Web Search Data into a Local LLM</title>
      <dc:creator>Tomás Murúa</dc:creator>
      <pubDate>Tue, 20 Oct 2026 01:52:22 +0000</pubDate>
      <link>https://dev.to/serpapi/how-to-fit-more-web-search-data-into-a-local-llm-2n8e</link>
      <guid>https://dev.to/serpapi/how-to-fit-more-web-search-data-into-a-local-llm-2n8e</guid>
      <description>&lt;p&gt;A local model can only use the web search results that fit in its &lt;a href="https://www.ibm.com/think/topics/context-window" rel="noopener noreferrer"&gt;context window&lt;/a&gt;. When we built the app for &lt;a href="https://serpapi.com/blog/how-to-connect-your-local-llm-with-web-search-data/" rel="noopener noreferrer"&gt;How to Connect Your Local LLM with Web Search Data&lt;/a&gt;, a single SerpApi response was often bigger than the whole context window available. So we trimmed every response with the &lt;a href="https://serpapi.com/json-restrictor" rel="noopener noreferrer"&gt;JSON Restrictor&lt;/a&gt;, which keeps only the fields the model needs and drops the rest on SerpApi’s servers before it reaches the LLM.&lt;/p&gt;

&lt;p&gt;SerpApi's new &lt;a href="https://serpapi.com/markdown-output" rel="noopener noreferrer"&gt;Markdown output&lt;/a&gt; changes that budget. The same data takes fewer tokens in Markdown than in JSON, so the restrictor can keep more of it. For most searches, the full response now fits with no restrictor at all.&lt;/p&gt;

&lt;p&gt;This blog will show you how to apply the JSON Restrictor to Markdown output, and when the full response is worth the wait. We'll use the same &lt;a href="https://github.com/serpapi/local-llm-web-search" rel="noopener noreferrer"&gt;open-source app&lt;/a&gt; that gives a local model running in &lt;a href="https://lmstudio.ai" rel="noopener noreferrer"&gt;LM Studio&lt;/a&gt; internet access. The app is built with &lt;a href="https://tanstack.com/start" rel="noopener noreferrer"&gt;TanStack Start&lt;/a&gt;, and we upgraded it to Start 1.0 along the way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6j60b73m7zr8c4nufoo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6j60b73m7zr8c4nufoo.gif" alt="How the local agent searches the web: the model runs in LM Studio on your Mac, and only the search query goes to SerpApi" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/serpapi/local-llm-web-search" rel="noopener noreferrer"&gt;GitHub - serpapi/local-llm-web-search: Give a local LLM real-time web search via SerpApi function calling.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s New Since the First Blog
&lt;/h2&gt;

&lt;p&gt;SerpApi's Markdown output returns curated results as a Markdown document instead of JSON. You add &lt;code&gt;output=md&lt;/code&gt; to a request, and the results come back as headings, lists and tables without the JSON keys, quotes and braces. It works across SerpApi's&lt;a href="https://serpapi.com/search-engine-apis" rel="noopener noreferrer"&gt;100+ APIs&lt;/a&gt;, and a Markdown search costs the same as a JSON search. The launch blog &lt;a href="https://serpapi.com/blog/introducing-serpapi-markdown-output/" rel="noopener noreferrer"&gt;Introducing SerpApi’s New Markdown Output&lt;/a&gt; covers the format and its token savings across our engines.&lt;/p&gt;

&lt;p&gt;The app now lets you choose what the model reads after each search. The benchmark compares four outputs, and a model only ever reads three of them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;What the model reads&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON (Raw)&lt;/td&gt;
&lt;td&gt;The full JSON response. The benchmark measures it, but no model reads it&lt;/td&gt;
&lt;td&gt;No restrictor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON (Restrictor)&lt;/td&gt;
&lt;td&gt;JSON trimmed to the fields the model needs, as in the first blog&lt;/td&gt;
&lt;td&gt;&lt;code&gt;json_restrictor&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown (Restrictor)&lt;/td&gt;
&lt;td&gt;Markdown with more results and more fields, at about the same size. This is the app's default&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;json_restrictor&lt;/code&gt; and &lt;code&gt;output=md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown (Raw)&lt;/td&gt;
&lt;td&gt;The full Markdown document&lt;/td&gt;
&lt;td&gt;&lt;code&gt;output=md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The setup changed too. The first blog ran Qwen3.5-9B on a MacBook Pro M4 Max with 36 GB of memory. This time I ran four of LM Studio's staff picks on a MacBook Pro M5 Pro with 48 GB, using &lt;a href="https://lmstudio.ai/link" rel="noopener noreferrer"&gt;LM Link&lt;/a&gt; to connect both MacBooks. They are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://lmstudio.ai/models/qwen/qwen3.8-27b" rel="noopener noreferrer"&gt;Qwen 3.8 27B&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lmstudio.ai/models/meta/muse-glimmer" rel="noopener noreferrer"&gt;Meta Muse Glimmer 30B&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lmstudio.ai/models/prism-ml/bonsai-27b" rel="noopener noreferrer"&gt;Prism Bonsai 27B&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lmstudio.ai/models/google/gemma-4-12b-qat" rel="noopener noreferrer"&gt;Gemma 4 12B QAT&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5ipaewwpe6uwwy178nu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5ipaewwpe6uwwy178nu.png" alt="LM Studio's staff picks, with Qwen 3.8 27B, Muse Glimmer, Bonsai 27B and Gemma 4 12B QAT at the top" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Tested
&lt;/h2&gt;

&lt;p&gt;Every model ran with a 32K context window, the same size as in the first blog. LM Studio loads a model with an 8K window by default, which is too small once a chat carries search results. A 32K window is enough for a long conversation, and it fits a 27B model in memory on a 36 GB Mac.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the Benchmark Measures
&lt;/h3&gt;

&lt;p&gt;The app's Benchmark page measures two things, and this blog uses both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Payload size:&lt;/strong&gt; The same seven searches run once for each of the four outputs, with no model involved. There's one search per tool, covering&lt;a href="https://serpapi.com/search-api" rel="noopener noreferrer"&gt;Search&lt;/a&gt;,&lt;a href="https://serpapi.com/google-finance-api" rel="noopener noreferrer"&gt;Finance&lt;/a&gt;,&lt;a href="https://serpapi.com/google-news-api" rel="noopener noreferrer"&gt;News&lt;/a&gt;,&lt;a href="https://serpapi.com/google-maps-api" rel="noopener noreferrer"&gt;Maps&lt;/a&gt;,&lt;a href="https://serpapi.com/google-flights-api" rel="noopener noreferrer"&gt;Flights&lt;/a&gt;,&lt;a href="https://serpapi.com/google-hotels-api" rel="noopener noreferrer"&gt;Hotels&lt;/a&gt; and&lt;a href="https://serpapi.com/google-shopping-api" rel="noopener noreferrer"&gt;Shopping&lt;/a&gt;. This shows what each output costs before a model reads anything. These sizes come from OpenAI's &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken&lt;/a&gt; library with its &lt;code&gt;cl100k_base&lt;/code&gt; encoding, which splits text into tokens. Each local model splits text its own way, so treat these sizes as estimates for comparing formats.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fydr6mu12cll2myvfxyp9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fydr6mu12cll2myvfxyp9.png" alt="The app's Payload size benchmark, comparing seven search results in four outputs" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;End to end:&lt;/strong&gt; Thirteen questions run through the whole app on each model, once for each of the three outputs a model can read. Seven need live data from SerpApi, one search per tool. Two need two searches at once, such as comparing AAPL with TSLA. The other four need no search, such as "&lt;em&gt;What is 15% of 240?&lt;/em&gt;", so they show how the model behaves on its own. We used it to compare how each model handles each output, how long it takes, and what it answers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F61esoclrqhn31rm9dbrk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F61esoclrqhn31rm9dbrk.png" alt="The app's End to end benchmark for Gemma 4 12B QAT, with 13 questions in three outputs" width="800" height="1666"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Much Does Markdown Save?
&lt;/h2&gt;

&lt;p&gt;To see what the format alone saves, we kept the restrictor strings from the first blog and sent the same seven searches as JSON and as Markdown. The seven results took 4,668 tokens in JSON and 3,412 in Markdown, 27% fewer. Here's how each tool changed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;JSON&lt;/th&gt;
&lt;th&gt;Markdown&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flights&lt;/td&gt;
&lt;td&gt;823&lt;/td&gt;
&lt;td&gt;439&lt;/td&gt;
&lt;td&gt;−47%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finance&lt;/td&gt;
&lt;td&gt;521&lt;/td&gt;
&lt;td&gt;324&lt;/td&gt;
&lt;td&gt;−38%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;News&lt;/td&gt;
&lt;td&gt;678&lt;/td&gt;
&lt;td&gt;422&lt;/td&gt;
&lt;td&gt;−38%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;785&lt;/td&gt;
&lt;td&gt;586&lt;/td&gt;
&lt;td&gt;−25%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hotels&lt;/td&gt;
&lt;td&gt;567&lt;/td&gt;
&lt;td&gt;433&lt;/td&gt;
&lt;td&gt;−24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maps&lt;/td&gt;
&lt;td&gt;497&lt;/td&gt;
&lt;td&gt;449&lt;/td&gt;
&lt;td&gt;−10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shopping&lt;/td&gt;
&lt;td&gt;797&lt;/td&gt;
&lt;td&gt;759&lt;/td&gt;
&lt;td&gt;−5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4,668&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3,412&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−27%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The saving depends on how deeply the data is nested. A flight option holds legs, and each leg holds airports and times, so JSON repeats keys, quotes and braces at every level. Markdown turns lists of results into tables and writes each column name once. Shopping results are flat, with a title, a price and a store per product, so there is little to remove.&lt;/p&gt;

&lt;p&gt;So instead of keeping that saving, we spend it. In Markdown, the same token budget can carry more fields and more results, so the Markdown output gets a looser restrictor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply the JSON Restrictor to Markdown Output
&lt;/h2&gt;

&lt;p&gt;There's no separate restrictor for Markdown. You send the same &lt;code&gt;json_restrictor&lt;/code&gt; parameter together with &lt;code&gt;output=md&lt;/code&gt;, and SerpApi filters the sections first, then renders what's left as Markdown.&lt;/p&gt;

&lt;p&gt;In the app, each tool has two restrictor strings. JSON sends the original one, and Markdown sends a looser one with more results and more fields. The Markdown request is the same search with &lt;code&gt;output=md&lt;/code&gt; added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pick the restrictor for the output mode. Markdown (Raw) sends none.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;restrictorFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RestrictorTool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ToolOutput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;markdown-full&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;markdown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;MARKDOWN_RESTRICTORS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RESTRICTORS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Same parameters as the JSON request, plus output=md.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;serpApiMarkdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://serpapi.com/search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;output&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;md&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SERPAPI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool spends the saving on different extras:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;JSON (Restrictor) keeps&lt;/th&gt;
&lt;th&gt;Markdown (Restrictor) keeps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;Answer box, &lt;a href="https://serpapi.com/google-ai-overview-api" rel="noopener noreferrer"&gt;AI Overview&lt;/a&gt; (6 blocks), 3 related questions, knowledge graph, 5 web results&lt;/td&gt;
&lt;td&gt;5 web results, answer box with its lists, AI Overview (8 blocks), knowledge graph, 3 top stories, 3 related questions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finance&lt;/td&gt;
&lt;td&gt;Price, key stats, 3 news items&lt;/td&gt;
&lt;td&gt;Price, key stats, company description, 5 news items&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;News&lt;/td&gt;
&lt;td&gt;5 stories, 1 related article each&lt;/td&gt;
&lt;td&gt;7 stories, 2 related articles each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maps&lt;/td&gt;
&lt;td&gt;5 places&lt;/td&gt;
&lt;td&gt;6 places, with opening hours and a description&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flights&lt;/td&gt;
&lt;td&gt;3 best and 3 other options&lt;/td&gt;
&lt;td&gt;4 best and 4 other options, with aircraft, legroom, overnight layovers and price insights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hotels&lt;/td&gt;
&lt;td&gt;5 hotels&lt;/td&gt;
&lt;td&gt;7 hotels, with a description and a location rating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shopping&lt;/td&gt;
&lt;td&gt;5 products, plus 3 inline results&lt;/td&gt;
&lt;td&gt;7 products, plus 3 inline results, with second-hand condition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, here are both restrictor strings for Maps, from &lt;code&gt;restrictors.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// JSON (Restrictor): five places.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RESTRICTORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;google_maps_search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local_results[0:5].{title,rating,reviews,type,address,price,open_state,phone,website,gps_coordinates}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;place_results.{title,rating,reviews,type,address,price,open_state,phone,website}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search_metadata.google_maps_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// ...one restrictor per tool&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Markdown (Restrictor): six places, with opening hours and a description.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MARKDOWN_RESTRICTORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;google_maps_search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;local_results[0:6].{title,rating,reviews,type,address,price,open_state,hours,phone,website,description}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;place_results.{title,rating,reviews,type,address,price,open_state,hours,phone,website,description}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search_metadata.google_maps_url&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="c1"&gt;// ...one restrictor per tool&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extra data shows up in the answers. Asked for the best barbecue in Austin, Qwen 3.8 27B listed five places with JSON and six with Markdown. The new one, Good BBQ Company, was one of the two highest rated. The Markdown answer also described Terry Black's as "4th-generation pitmasters, old-school smoked meats," from the place descriptions only the Markdown restrictor keeps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4ohi22ayf0gklzzysvz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn4ohi22ayf0gklzzysvz.png" alt="Qwen 3.8 27B's answer for the best barbecue in Austin with Markdown (Restrictor), listing six places" width="800" height="763"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When You Skip the JSON Restrictor
&lt;/h2&gt;

&lt;p&gt;The bigger change is the full response, which I called Raw in the app. In the first blog, sending a raw SerpApi response to a local model wasn't an option. The seven raw JSON responses added up to 326,949 tokens, almost 10 times the context window. The same seven responses in Markdown came to 71,244 tokens, 4.6 times smaller, and none of them was bigger than the window.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F753hzexh9k98yzgtyx3z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F753hzexh9k98yzgtyx3z.png" alt="Raw search result size for seven tools, where every Markdown response stays under the 32K context window" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fitting in the window doesn't make it fast. With Markdown (Raw), the models read two to three times as many tokens as with Markdown (Restrictor), and took up to 2.7 times as long to answer.&lt;/p&gt;

&lt;p&gt;Two full pages in one question can still overflow the context window. On one of the four models, the prompt comparing AAPL and TSLA went over 32K tokens. LM Studio cut part of it without an error, and the answer got Apple’s daily change wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Output Should You Use?
&lt;/h2&gt;

&lt;p&gt;Most local chats should start with Markdown using the restrictor, and the other outputs fit narrower cases.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markdown (Restrictor):&lt;/strong&gt; The default. Use it whenever a model reads the results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON (Restrictor):&lt;/strong&gt; Use it when your code reads the results, for example to sort by price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown (Raw):&lt;/strong&gt; Use it when an answer needs the whole page and can wait up to three times longer. Avoid it for questions that need two big searches, such as comparing two stocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON (Raw):&lt;/strong&gt; Don't send it to a local model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your hardware has the memory, you can raise the 32K window with the app’s &lt;code&gt;LMSTUDIO_CONTEXT_LENGTH&lt;/code&gt; setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Four Models Compare
&lt;/h2&gt;

&lt;p&gt;All four models finished every question. The times below are the average per search question in Markdown (Restrictor):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Qwen 3.8 27B&lt;/strong&gt; is the app's default model when it's installed. In long test conversations, it turned follow-ups like "around there" and "which of those" into the right searches. It took about 42 seconds per question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta Muse Glimmer 30B&lt;/strong&gt; splits text into fewer tokens than the other three, so the same search results take less of its window. It was the slowest, about 62 seconds per question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prism Bonsai 27B&lt;/strong&gt; is a 27B model that LM Studio lists at about 4 GB, and it took about 24 seconds per question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 12B QAT&lt;/strong&gt; is the smallest model here, at 12B parameters, and the fastest, at about 16 seconds per question, or about 43 with Markdown (Raw). In long conversations it had the most problems, such as giving a price that wasn’t in the search results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With less memory than the 48 GB used here, the app may load a smaller window, so less search data fits. New open models also come out almost every week, so the best choice changes fast. The staff picks in&lt;a href="https://lmstudio.ai/docs/app/basics/download-model" rel="noopener noreferrer"&gt;LM Studio's model search&lt;/a&gt; are a good place to start, because LM Studio matches them to your hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Search Results Contradict What the Model Knows
&lt;/h2&gt;

&lt;p&gt;Every model stops learning at its training cutoff, so it doesn’t know what happened after that date. On September 1, 2026, John Ternus became Apple's CEO and Tim Cook became executive chairman, after 15 years as CEO. So "&lt;em&gt;Who is the current CEO of Apple?&lt;/em&gt;" puts a model's training against the search results.&lt;/p&gt;

&lt;p&gt;Here's what Qwen 3.8 27B thought before its first search:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The user is asking who the current CEO of Apple is. This is a factual question. As of my knowledge, Tim Cook has been Apple's CEO since 2011. However, since this is a question about the current situation, I should verify with a search to make sure there haven't been any changes. Let me search.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That part is right. The model knows its knowledge can be out of date, and it checks. The harder part comes when the results disagree with it. In an earlier run, the AI Overview in Qwen's first results already named John Ternus, and Qwen didn't accept it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This is a significant claim - it suggests Tim Cook stepped down and John Ternus took over. Today's date is 2026-10-02, so this would be about a month ago. Let me verify this with a news search to make sure this is accurate and not a hallucination.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We can read all of this because the models run locally. LM Studio writes each model's reasoning to its server log, word for word. Hosted reasoning models usually show you a summary instead. When OpenAI launched o1, it&lt;a href="https://openai.com/index/learning-to-reason-with-llms/#:~:text=0.976-,Hiding%20the%20Chains%20of%20Thought,-We%20believe%20that" rel="noopener noreferrer"&gt;chose not to show the raw chain of thought&lt;/a&gt;, and other AI labs have done the same recently. One competitive risk is &lt;a href="https://www.ibm.com/think/topics/knowledge-distillation" rel="noopener noreferrer"&gt;knowledge distillation&lt;/a&gt;, where a smaller model learns to copy a bigger model's outputs, and full reasoning is valuable material for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this blog we added SerpApi’s Markdown output to the app and benchmarked it on four local models. For the same fields, Markdown took 27% fewer tokens than JSON, so the restrictor could keep more results for about the same budget. Even without a restrictor, all seven full Markdown responses fit in the 32K window.&lt;/p&gt;

&lt;p&gt;The same approach works for any SerpApi engine. Add &lt;code&gt;output=md&lt;/code&gt; to the request, keep one restrictor per engine, and spend the tokens Markdown saves on the fields your questions need. To try it, clone the repo and add your SerpApi API key.&lt;a href="https://serpapi.com/users/sign_up?plan=free" rel="noopener noreferrer"&gt;A free account&lt;/a&gt; includes 250 searches a month.&lt;/p&gt;

&lt;p&gt;When the next model you want to try shows up in LM Studio, run it through the app's Benchmark page and compare it with these four.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>How to build a web scraper: A beginner's guide</title>
      <dc:creator>Hilman Ramadhan</dc:creator>
      <pubDate>Sat, 17 Oct 2026 17:34:05 +0000</pubDate>
      <link>https://dev.to/serpapi/how-to-build-a-web-scraper-a-beginners-guide-18c0</link>
      <guid>https://dev.to/serpapi/how-to-build-a-web-scraper-a-beginners-guide-18c0</guid>
      <description>&lt;p&gt;The simple definition of web scraping is the automated process of extracting data from websites. It also usually involves saving that data into a structured format, like a database, CSV or JSON file, or text document. That way, you can parse large collections of pages (or entire websites) in a much more efficient way than a person browsing the pages and manually copying the desired information.&lt;/p&gt;

&lt;p&gt;A web scraper first needs to make requests to a web server, and then fetch the contents of the requested page, just like a person using a web browser normally would. Then, it needs to parse the page into whichever format is required. Both of those steps can be tricky at scale—many servers will throttle or block IP addresses making bulk requests, and parsers have to understand various page layouts and rendering methods.&lt;/p&gt;

&lt;p&gt;In this guide, we'll go over how to build your own web scraper in Python, JavaScript, and Ruby, and how to use the easier option—the &lt;a href="https://serpapi.com/extract-api" rel="noopener noreferrer"&gt;Extract API from SerpApi&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building your own web scraper
&lt;/h2&gt;

&lt;p&gt;Web scraping can be done using a variety of programming languages and tools. Whether you're comfortable with JavaScript, Ruby, Go, or another language, libraries and frameworks are available to help you scrape websites.&lt;/p&gt;

&lt;p&gt;The first step in web scraping is to fetch the HTML content of the web page you want. This is similar to how your web browser works when you visit a website—it sends a request to the server, and the server responds with the HTML, CSS, JavaScript, and other resources needed to display the page.&lt;/p&gt;

&lt;p&gt;In web scraping, you'll use a programming language or tool to send an HTTP request to the website's server. This can be done using various methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Programming Libraries&lt;/strong&gt; : Languages like Python, JavaScript, Ruby, and others have libraries that make it easy to send HTTP requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in Functions&lt;/strong&gt; : Some languages have built-in functions for making web requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By requesting the HTML content, you're retrieving the raw data that makes up the web page, which you can then process and analyze.&lt;/p&gt;

&lt;p&gt;Once you've obtained the HTML content, the next step is to parse it to extract the information you're interested in. Web pages are structured documents that use HTML tags to define elements like headings, paragraphs, links, images, and more.&lt;/p&gt;

&lt;p&gt;Some common methods of navigating through HTML pages are &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors" rel="noopener noreferrer"&gt;CSS selectors&lt;/a&gt; or &lt;a href="https://serpapi.com/blog/xpath-selector-cheat-sheet-practical-examples-included/" rel="noopener noreferrer"&gt;XPath&lt;/a&gt;. For example, if you wanted to find all links in a page with "product" in the destination URL, you could use a CSS selector like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;href&lt;/span&gt;&lt;span class="o"&gt;*=&lt;/span&gt;&lt;span class="s1"&gt;"product"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Libraries like &lt;a href="https://pypi.org/project/beautifulsoup4/" rel="noopener noreferrer"&gt;Beautiful Soup&lt;/a&gt; for Python, &lt;a href="https://www.npmjs.com/package/jsdom" rel="noopener noreferrer"&gt;jsdom&lt;/a&gt; or &lt;a href="https://cheerio.js.org/" rel="noopener noreferrer"&gt;Cheerio&lt;/a&gt; for Node.js, &lt;a href="https://github.com/sparklemotion/nokogiri" rel="noopener noreferrer"&gt;Nokogiri&lt;/a&gt; or &lt;a href="https://github.com/serpapi/nokolexbor" rel="noopener noreferrer"&gt;Nokolexbor&lt;/a&gt; for Ruby, and others can parse HTML and XML documents. That way, you can navigate the document elements using standard selectors. Another option is &lt;a href="https://serpapi.com/blog/web-scraping-with-ai-parsing-html-to-structured-data/" rel="noopener noreferrer"&gt;using an AI model to parse the HTML&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once you've identified the correct elements, you can extract the text, attributes, or other relevant information. That gives you the data for analyzing trends, feeding into another application, building a dataset, or other tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practice scraping a real website
&lt;/h3&gt;

&lt;p&gt;It's time to build a web scraper! We will use the &lt;a href="https://news.ycombinator.com/" rel="noopener noreferrer"&gt;Hacker News home page&lt;/a&gt; as the example site, with the goal of extracting the title, external link, and visible link label for each post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3pkhyci2mlzduesjgsj6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3pkhyci2mlzduesjgsj6.png" alt="The Hacker News home page in a web browser." width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, we need to figure out where the links are in the page's HTML. Try right-clicking one of the links, then clicking the "Inspect Element" or "Inspect" menu option.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🧭 &lt;strong&gt;Using Safari on Mac?&lt;/strong&gt; You may need to enable development features first. From the top menu bar, select Safari &amp;gt; Settings, click the Advanced tab, then check the box for "Show features for web developers."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This will open the HTML page's structure, and jump to where that link element is located.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02vnuulm9z7xpusihgwz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F02vnuulm9z7xpusihgwz.png" alt="The Elements view in Safari, showing the page structure." width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As of the time of writing, each submission is a &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt; table row element with a &lt;code&gt;athing&lt;/code&gt; CSS class. In each of those table rows, there's a &lt;code&gt;.titleline&lt;/code&gt; element containing everything we need: a link element (&lt;code&gt;a&lt;/code&gt;) with the external link and post title, and a &lt;code&gt;.sitestr&lt;/code&gt; tag with the visible link label.&lt;/p&gt;

&lt;p&gt;Now it's time to scrape that information!&lt;/p&gt;

&lt;h3&gt;
  
  
  Web scraping using Python
&lt;/h3&gt;

&lt;p&gt;If you want to build a web scraper in Python, you can start with the &lt;a href="https://pypi.org/project/requests/" rel="noopener noreferrer"&gt;Requests&lt;/a&gt; and &lt;a href="https://pypi.org/project/beautifulsoup4/" rel="noopener noreferrer"&gt;Beautiful Soup&lt;/a&gt; libraries. For more complex web pages beyond this example, you can try &lt;a href="https://www.selenium.dev/" rel="noopener noreferrer"&gt;Selenium&lt;/a&gt; or &lt;a href="https://www.scrapy.org/" rel="noopener noreferrer"&gt;Scrapy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First, &lt;a href="https://www.python.org/downloads/" rel="noopener noreferrer"&gt;install Python on your computer&lt;/a&gt; if you haven't already, then create the folder you want to use for this project. Next, open the Terminal (Mac/Linux) or PowerShell (Windows) application, and change the current directory to that project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Desktop/scraper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📁 &lt;strong&gt;Not sure how to find a folder's path?&lt;/strong&gt; Type &lt;code&gt;cd&lt;/code&gt; and a space, then drag the folder into your Terminal or PowerShell window.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, create a &lt;a href="https://docs.python.org/3/library/venv.html" rel="noopener noreferrer"&gt;Python virtual environment&lt;/a&gt; for the project, so all dependencies and other data stays within the folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💻 &lt;strong&gt;Python command not found?&lt;/strong&gt; Try "python3" instead. The name of the executable depends on how it was installed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, activate the Python virtual environment. On a Mac with the default bash/zsh shell, run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a Windows PC in PowerShell, run this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;venv\Scripts\Activate.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, install the Beautiful Soup and Requests libraries in the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests beautifulsoup4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now it's time to start writing code! Create a new Python script called &lt;code&gt;main.py&lt;/code&gt; in your project with the following contents, then try running it with the &lt;code&gt;python main.py&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://news.ycombinator.com/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;html_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This imports the Requests library, sends a GET request to the URL, and stores the HTML content in a variable.&lt;/p&gt;

&lt;p&gt;Now, let's parse the HTML to extract the data. Add the following import statement to the top of the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the &lt;code&gt;print(html_content)&lt;/code&gt; statement with this line, which creates a Beautiful Soup object for the parsed HTML content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, the script needs to get a list of all table rows with the &lt;code&gt;athing&lt;/code&gt; class, and extract each row's link URL, inner text, and visible link. You can do this with a &lt;code&gt;soup.find_all&lt;/code&gt; function and a &lt;code&gt;for&lt;/code&gt; loop at the end of the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;athing&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;titleline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;span&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;titleline&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;link_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;titleline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;visible_link_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;span&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sitestr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# This avoids an error when the visible_link_tag is None
&lt;/span&gt;    &lt;span class="n"&gt;visible_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visible_link_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;visible_link_tag&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No visible link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Title:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Link:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Visible Link:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visible_link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running the entire script should give you an output like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;Title:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;StreetComplete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;iOS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;now&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;beta&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Link:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://github.com/streetcomplete/StreetComplete/issues/&lt;/span&gt;&lt;span class="mi"&gt;5421&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Visible&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Link:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;github.com/streetcomplete&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Title:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;RIP,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;vector&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;database&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Link:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://turbopuffer.com/blog/rip-vector-database&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Visible&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Link:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;turbopuffer.com&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Title:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;How&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;speed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Rust&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;compiler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;September&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Link:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://nnethercote.github.io/&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;09&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="err"&gt;/how-to-speed-up-the-rust-compiler-in-september&lt;/span&gt;&lt;span class="mi"&gt;-2026&lt;/span&gt;&lt;span class="err"&gt;.html&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Visible&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Link:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;nnethercote.github.io&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;---&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The website has multiple pages. If we want to grab all the data, we must first find the pattern. In this case, this is what the patterns look like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page 1: &lt;a href="https://news.ycombinator.com/?p=1" rel="noopener noreferrer"&gt;https://news.ycombinator.com/?p=1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Page 2: &lt;a href="https://news.ycombinator.com/?p=2" rel="noopener noreferrer"&gt;https://news.ycombinator.com/?p=2&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Page n: &lt;a href="https://news.ycombinator.com/?p=n" rel="noopener noreferrer"&gt;https://news.ycombinator.com/?p=n&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can check each page by wrapping the entire script in another &lt;code&gt;for&lt;/code&gt; loop that counts from 1 to 3, and then runs the GET request with that number as the page count in the URL. The full script should now look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://news.ycombinator.com/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;max_pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="c1"&gt;# I gave the limit to only 3 pages
&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_pages&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?p=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;html_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
    &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;athing&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;titleline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;span&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;titleline&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;link_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;titleline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
        &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;visible_link_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;span&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sitestr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;visible_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visible_link_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;visible_link_tag&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No visible link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Title:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Link:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Visible Link:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visible_link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;---&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output will now include results from the first three pages.&lt;/p&gt;

&lt;p&gt;You probably want to save the data somewhere other than the terminal output. &lt;a href="https://docs.python.org/3/library/csv.html" rel="noopener noreferrer"&gt;Python's built-in CSV library&lt;/a&gt; can be used to write a CSV spreadsheet file, which can then be opened by Google Sheets, Microsoft Excel, LibreOffice Calc, and many other applications.&lt;/p&gt;

&lt;p&gt;Here's how you could write a CSV file with the title, link, and visible link as the columns, and each post as a row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;

&lt;span class="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://news.ycombinator.com/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;max_pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="c1"&gt;# Open a CSV file in write mode
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data.csv&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Write the header row
&lt;/span&gt;    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Link&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Visible Link&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_pages&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?p=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;html_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
        &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;athing&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;titleline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;span&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;titleline&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;link_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;titleline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
            &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link_tag&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="n"&gt;visible_link_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;span&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sitestr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;visible_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visible_link_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;visible_link_tag&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No visible link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

            &lt;span class="c1"&gt;# Write the data row to the CSV file
&lt;/span&gt;            &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visible_link&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, running the script will create a &lt;code&gt;data.csv&lt;/code&gt; file in your project folder with all the data. The file looks like this when opened in a spreadsheet application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frg7l8v4kxdlckyfjgw7z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frg7l8v4kxdlckyfjgw7z.png" alt="A spreadsheet open in Apple Numbers, containing rows of titles, links, and visible links." width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You now have a basic web scraper running in Python! The below resources can help you take on more complex scraping tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://serpapi.com/blog/web-scraping-with-css-selectors-using-python/" rel="noopener noreferrer"&gt;&lt;strong&gt;Web scraping with CSS selectors using Python&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;:&lt;/strong&gt; A guide on using CSS selectors to find and parse HTML elements.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://serpapi.com/blog/selenium-web-scraping-python/" rel="noopener noreferrer"&gt;&lt;strong&gt;Selenium web scraping&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;:&lt;/strong&gt; Web pages that load with JavaScript or other dynamic elements won't work with only GET requests and Beautiful Soup, but you can parse them with Selenium.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://serpapi.com/blog/python-web-scraping-tutorial/" rel="noopener noreferrer"&gt;&lt;strong&gt;Web scraping in Python&lt;/strong&gt;&lt;/a&gt;: A more in-depth guide on building scrapers with Python.&lt;/li&gt;
&lt;li&gt;The documentation for &lt;a href="https://www.crummy.com/software/BeautifulSoup/bs4/doc/" rel="noopener noreferrer"&gt;Beautiful Soup&lt;/a&gt;, &lt;a href="https://docs.python-requests.org/en/latest/" rel="noopener noreferrer"&gt;Requests&lt;/a&gt;, and &lt;a href="https://docs.python.org/3/library/csv.html" rel="noopener noreferrer"&gt;Python's CSV library&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://serpapi.com/blog/13-ways-to-scrape-any-data-from-any-website/" rel="noopener noreferrer"&gt;&lt;strong&gt;13 ways to scrape public data&lt;/strong&gt;&lt;/a&gt;: More methods for parsing complex web pages in Python.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also skip to the Extract API for a simpler method of web scraping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web scraping using JavaScript
&lt;/h3&gt;

&lt;p&gt;JavaScript is also an option for web scraping projects, though not in the same way you might use the language in front-end development. Web browsers will block access to most external domains with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" rel="noopener noreferrer"&gt;CORS restrictions&lt;/a&gt;, which is required for scraping, so you need to use Node.js or another non-browser runtime. Those runtimes typically don't have a built-in &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/DOM" rel="noopener noreferrer"&gt;Document Object Model&lt;/a&gt; (DOM), so common functions for parsing HTML like &lt;code&gt;querySelector&lt;/code&gt; or &lt;code&gt;getElementById()&lt;/code&gt; are unavailable unless you add additional libraries.&lt;/p&gt;

&lt;p&gt;For this web scraping project, we'll use the Node.js runtime with the &lt;a href="https://www.npmjs.com/package/jsdom" rel="noopener noreferrer"&gt;jsdom&lt;/a&gt; library. Node.js has a built-in &lt;a href="https://nodejs.org/learn/getting-started/fetch" rel="noopener noreferrer"&gt;Fetch API&lt;/a&gt; for retrieving web content, so additional libraries for that purpose (like Axios) are no longer required.&lt;/p&gt;

&lt;p&gt;First, you'll need &lt;a href="https://nodejs.org/en/download/" rel="noopener noreferrer"&gt;Node.js and the NPM package manager&lt;/a&gt; installed on your computer. Then create a folder somewhere for your project, open a Terminal window (Mac/Linux) or PowerShell prompt (Windows), and navigate to that folder. For example, this will change the current directory to a "scraper" folder on your Desktop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Desktop/scraper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📁 &lt;strong&gt;Not sure how to find a folder's path?&lt;/strong&gt; Type &lt;code&gt;cd&lt;/code&gt; and a space, then drag the folder into your Terminal or PowerShell window.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, we need to install the jsdom library in the project, with this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;jsdom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a new JavaScript file called &lt;code&gt;main.js&lt;/code&gt; in the project folder, and start with this code that fetches the HTML for the home page and displays it in the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://news.ycombinator.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run it with the &lt;code&gt;node main.js&lt;/code&gt; command. Next, we can parse the HTML by importing the jsdom library, passing the text to a JSDOM constructor, and making a &lt;code&gt;for&lt;/code&gt; loop that goes through every &lt;code&gt;.athing&lt;/code&gt; table row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jsdom&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsdom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://news.ycombinator.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;jsdom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JSDOM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Get every table row representing a post&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tr.athing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get the title, link, and visible link&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.titleline a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.titleline a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visibleLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.sitestr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No visible link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Display the data&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Title: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nLink: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nVisible Link: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;visibleLink&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n---`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The website has multiple pages. If we want to grab all the data, we must first find the pattern. In this case, this is what the patterns look like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page 1: &lt;a href="https://news.ycombinator.com/?p=1" rel="noopener noreferrer"&gt;https://news.ycombinator.com/?p=1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Page 2: &lt;a href="https://news.ycombinator.com/?p=2" rel="noopener noreferrer"&gt;https://news.ycombinator.com/?p=2&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Page n: &lt;a href="https://news.ycombinator.com/?p=n" rel="noopener noreferrer"&gt;https://news.ycombinator.com/?p=n&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a modified version of the script that fetches the first three pages, and then displays all links from all pages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jsdom&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsdom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://news.ycombinator.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/?p=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;jsdom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JSDOM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Get every table row representing a post&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tr.athing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Get the title, link, and visible link&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.titleline a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.titleline a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visibleLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.sitestr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No visible link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Display the data&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Title: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nLink: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nVisible Link: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;visibleLink&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n---`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might want to save the scraped data outside of the terminal output. We can write the results to a CSV file by importing Node's fs library and constructing the CSV format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jsdom&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;jsdom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://news.ycombinator.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;csvText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`"Title","Link","Visible Link"`&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/?p=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;jsdom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;JSDOM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Get every table row representing a post&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;dom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tr.athing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Get the title, link, and visible link&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.titleline a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.titleline a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visibleLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.sitestr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No visible link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Add the data as a new row&lt;/span&gt;
        &lt;span class="nx"&gt;csvText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`\n"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;visibleLink&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data.csv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;csvText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, running the script will create a &lt;code&gt;data.csv&lt;/code&gt; file in your project folder with all the data. The file looks like this when opened in a spreadsheet application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fejsyucz27hffm5sv7qes.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fejsyucz27hffm5sv7qes.png" alt="A spreadsheet open in Apple Numbers, containing rows of titles, links, and visible links." width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You now have a basic web scraper running in Node.js with JavaScript! The below resources can help you take on more complex scraping tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Selectors_and_combinators" rel="noopener noreferrer"&gt;&lt;strong&gt;CSS selectors and combinators&lt;/strong&gt;&lt;/a&gt;: Learn about using CSS selectors to find elements, which also work in jsdom and other HTML parsing methods.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://serpapi.com/blog/web-scraping-dynamic-website-with-puppeteer/" rel="noopener noreferrer"&gt;&lt;strong&gt;Web scraping a dynamic website&lt;/strong&gt;&lt;/a&gt;: How to use Puppeteer with Node.js to scrape a website that uses a dynamic layout instead of static HTML.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://serpapi.com/blog/web-scraping-in-javascript-complete-tutorial-for-beginner/" rel="noopener noreferrer"&gt;&lt;strong&gt;Web scraping with JavaScript and Node.js&lt;/strong&gt;&lt;/a&gt;: A more detailed guide on web scraping techniques using JavaScript.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also skip to the Extract API for a simpler method of web scraping.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web scraping using Ruby
&lt;/h3&gt;

&lt;p&gt;Ruby is another language option for building a web scraping tool. You can use libraries like &lt;a href="https://ruby-doc.org/stdlib-2.6/libdoc/net/http/rdoc/Net/HTTP.html" rel="noopener noreferrer"&gt;Net::HTTP&lt;/a&gt; or &lt;a href="https://rubygems.org/gems/httparty" rel="noopener noreferrer"&gt;HTTParty&lt;/a&gt; for making GET requests to web pages, &lt;a href="https://github.com/serpapi/nokolexbor" rel="noopener noreferrer"&gt;Nokolexbor&lt;/a&gt; or &lt;a href="https://github.com/sparklemotion/nokogiri" rel="noopener noreferrer"&gt;Nokogiri&lt;/a&gt; for parsing HTML and XML elements, and &lt;a href="https://www.selenium.dev/" rel="noopener noreferrer"&gt;Selenium&lt;/a&gt; for scraping with an automated web browser.&lt;/p&gt;

&lt;p&gt;First, you need Ruby installed on your computer. You can download it from the &lt;a href="https://www.ruby-lang.org/en/downloads/" rel="noopener noreferrer"&gt;official website&lt;/a&gt; or use a version manager like &lt;a href="https://github.com/rbenv/rbenv" rel="noopener noreferrer"&gt;rbenv&lt;/a&gt; or &lt;a href="https://rvm.io/" rel="noopener noreferrer"&gt;RVM&lt;/a&gt;. Then create a folder somewhere for your project, open a Terminal window (Mac/Linux) or PowerShell prompt (Windows), and navigate to that folder. For example, this will change the current directory to a "scraper" folder on your Desktop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/Desktop/scraper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;📁 &lt;strong&gt;Not sure how to find a folder's path?&lt;/strong&gt; Type &lt;code&gt;cd&lt;/code&gt; and a space, then drag the folder into your Terminal or PowerShell window.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, create a plain text file called &lt;code&gt;Gemfile&lt;/code&gt; in your project folder to keep track of dependencies, with &lt;code&gt;source 'https://rubygems.org'&lt;/code&gt; as the only line. You can do that in a few different ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use a plain text editor (make sure not to add a &lt;code&gt;.txt&lt;/code&gt; extension to the file)&lt;/li&gt;
&lt;li&gt;For Windows PowerShell, run this command in the project folder: &lt;code&gt;Add-Content Gemfile "source 'https://rubygems.org'"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;For Mac/Linux, run this command in the project folder: &lt;code&gt;"source 'https://rubygems.org'" &amp;gt;&amp;gt; Gemfile&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We will use &lt;a href="https://rubygems.org/gems/httparty" rel="noopener noreferrer"&gt;HTTParty&lt;/a&gt; for fetching web pages, &lt;a href="https://rubygems.org/gems/nokolexbor" rel="noopener noreferrer"&gt;Nokolexbor&lt;/a&gt; for parsing HTML, and &lt;a href="https://rubygems.org/gems/csv" rel="noopener noreferrer"&gt;csv&lt;/a&gt; for writing the data to CSV files. The CSV gem is &lt;a href="https://stdgems.org/csv/" rel="noopener noreferrer"&gt;usually bundled with Ruby&lt;/a&gt;, but it should still be defined as a dependency. Make sure they are installed with this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle add httparty nokolexbor csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a Ruby file called &lt;code&gt;main.rb&lt;/code&gt; in your project folder. The first version fetches the home page for Hacker News using the &lt;code&gt;httparty&lt;/code&gt; gem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'httparty'&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://news.ycombinator.com/'&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;html_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;html_content&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can run it with the &lt;code&gt;ruby main.rb&lt;/code&gt; command. Next, we need to add a require line for &lt;code&gt;nokolexbor&lt;/code&gt; and then use it to parse each &lt;code&gt;.athing&lt;/code&gt; table row as a post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'httparty'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'nokolexbor'&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://news.ycombinator.com/'&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;html_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;
&lt;span class="n"&gt;parsed_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Nokolexbor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;parsed_page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tr.athing'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# Get the title, link, and visible link&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.titleline a'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
    &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.titleline a'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s1"&gt;'href'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;visible_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.sitestr'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.sitestr'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'No visible link'&lt;/span&gt;
    &lt;span class="c1"&gt;# Display the data&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Title: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Link: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Visible Link: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;visible_link&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;---"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The website has multiple pages. If we want to grab all the data, we must first find the pattern. In this case, this is what the patterns look like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Page 1: &lt;a href="https://news.ycombinator.com/?p=1" rel="noopener noreferrer"&gt;https://news.ycombinator.com/?p=1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Page 2: &lt;a href="https://news.ycombinator.com/?p=2" rel="noopener noreferrer"&gt;https://news.ycombinator.com/?p=2&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Page n: &lt;a href="https://news.ycombinator.com/?p=n" rel="noopener noreferrer"&gt;https://news.ycombinator.com/?p=n&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modifying the script to parse the first three pages isn't too difficult. We just need a loop that adds the current iteration to the page URL, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'httparty'&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'nokolexbor'&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://news.ycombinator.com/'&lt;/span&gt;
&lt;span class="n"&gt;max_pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;max_pages&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?p=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;html_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;
    &lt;span class="n"&gt;parsed_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Nokolexbor&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;parsed_page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tr.athing'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="c1"&gt;# Get the title, link, and visible link&lt;/span&gt;
        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.titleline a'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
        &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.titleline a'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s1"&gt;'href'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;visible_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.sitestr'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.sitestr'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'No visible link'&lt;/span&gt;
        &lt;span class="c1"&gt;# Display the data&lt;/span&gt;
        &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"Title: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Link: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Visible Link: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;visible_link&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;---"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might want to save the scraped data for future use. We can write the results to a CSV file by adding a require line for &lt;code&gt;csv&lt;/code&gt;, creating a header row and a row for each post, and then writing it to a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'csv'&lt;/span&gt;

&lt;span class="no"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'https://news.ycombinator.com/'&lt;/span&gt;
&lt;span class="n"&gt;max_pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="c1"&gt;# Open a CSV file in write mode&lt;/span&gt;
&lt;span class="no"&gt;CSV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'data.csv'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'w'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# Write the header row&lt;/span&gt;
  &lt;span class="n"&gt;csv&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Title'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Link'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Visible Link'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;max_pages&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTParty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?p=&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;html_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;
    &lt;span class="n"&gt;parsed_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Nokogiri&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed_page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'tr.athing'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;titleline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'span.titleline a'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;titleline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
      &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;titleline&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'href'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

      &lt;span class="n"&gt;visible_link_tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'span.sitestr'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;visible_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;visible_link_tag&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;visible_link_tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'No visible link'&lt;/span&gt;

      &lt;span class="c1"&gt;# Write the data row to the CSV file&lt;/span&gt;
      &lt;span class="n"&gt;csv&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;visible_link&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'Data successfully written to data.csv'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, running the script will create a &lt;code&gt;data.csv&lt;/code&gt; file in your project folder with all the data. The file looks like this when opened in a spreadsheet application:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjssn4vip3038w95iyba7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjssn4vip3038w95iyba7.png" alt="A spreadsheet open in Apple Numbers, containing rows of titles, links, and visible links." width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You now have a Ruby-powered web scraper! Check out these resources for help with more complex scraping tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Selectors_and_combinators" rel="noopener noreferrer"&gt;&lt;strong&gt;CSS selectors and combinators&lt;/strong&gt;&lt;/a&gt;: Learn about using CSS selectors to find elements, which are used by Nokolexbor, Nokogiri, and other HTML parsing methods.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.selenium.dev/documentation/" rel="noopener noreferrer"&gt;&lt;strong&gt;Selenium documentation&lt;/strong&gt;&lt;/a&gt;: For sites that load dynamically instead of providing static HTML, you need to use Selenium or another browser automation tool.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://serpapi.com/blog/web-scraping-with-ruby/" rel="noopener noreferrer"&gt;&lt;strong&gt;Web scraping with Ruby&lt;/strong&gt;&lt;/a&gt;: A more complete guide on web scraping methods while using Ruby.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also skip to the Extract API for a simpler method of web scraping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges with web scraping
&lt;/h2&gt;

&lt;p&gt;Basic web scraping, with pages already rendered in HTML with consistent element names, isn't too complicated in many programming languages and environments. However, you will inevitably run into problems as you try parsing different websites, or try requesting many pages.&lt;/p&gt;

&lt;p&gt;These are some of the common challenges with web scraping at scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Identifying data sources&lt;/strong&gt; : Determining where and how data is stored can be complex. Websites may store data across different pages or make requests over APIs, making it difficult to pinpoint the exact source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handling CAPTCHA tests&lt;/strong&gt; : Websites often use a &lt;a href="https://knowledge.workspace.google.com/admin/getting-started/what-is-captcha" rel="noopener noreferrer"&gt;CAPTCHA&lt;/a&gt;, short for "Completely Automated Public Turing test to tell Computers and Humans Apart," to block automated access. Bypassing these tests without violating terms of service requires sophisticated methods, such as using CAPTCHA-solving services, which adds cost and complexity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managing IP blocks and rate limits&lt;/strong&gt; : Frequent requests from the same IP can lead to blocks or bans. Using proxies to rotate IPs and implementing smart throttling mechanisms can help mitigate this but require careful management and additional infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal and ethical considerations&lt;/strong&gt; : Scraping must be done in compliance with laws and website terms of service. Ignoring these can lead to legal actions and ethical issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data structure changes&lt;/strong&gt; : Websites often update their layout and structure, which can break scrapers. Regularly maintaining and updating scraping scripts is crucial to handle these changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamically-rendered pages:&lt;/strong&gt; Many websites will render the page with JavaScript, instead of sending a complete HTML structure over the initial request. This requires fetching the page with an automated web browser, like &lt;a href="https://www.selenium.dev/" rel="noopener noreferrer"&gt;Selenium&lt;/a&gt; or &lt;a href="https://pptr.dev/" rel="noopener noreferrer"&gt;Puppeteer&lt;/a&gt;, instead of a simple GET request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The easy web scraping method
&lt;/h2&gt;

&lt;p&gt;If you don't want to build and maintain your own web scraper, you can use our &lt;a href="https://serpapi.com/extract-api" rel="noopener noreferrer"&gt;Extract API&lt;/a&gt;. It can parse any public web page and return its contents as structured JSON or Markdown, including automatic detection of headers, meta tags, inline links, and the complete page text. There's also a &lt;a href="https://serpapi.com/extract-map-mode" rel="noopener noreferrer"&gt;Map mode&lt;/a&gt; for finding all pages on a given website, and &lt;a href="https://serpapi.com/extract-crawl-mode" rel="noopener noreferrer"&gt;Crawl mode&lt;/a&gt; for finding &lt;em&gt;and&lt;/em&gt; parsing all of a website's pages.&lt;/p&gt;

&lt;p&gt;If you request a page on an engine or platform that has a dedicated SerpApi API, the results will be automatically routed to that API. For example, fetching a YouTube video URL with the Extract API will return all the data available through the &lt;a href="https://serpapi.com/youtube-video-api" rel="noopener noreferrer"&gt;YouTube Video API&lt;/a&gt;, like the video description and all recommended videos.&lt;/p&gt;

&lt;p&gt;If the built-in parsing from the Extract API isn't enough, you can still use it to retrieve the fully-rendered HTML, and then build your own parser using that input.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting started with SerpApi
&lt;/h3&gt;

&lt;p&gt;You need a free SerpApi account before you can use the Google Play Store APIs. You can &lt;a href="https://serpapi.com/pricing" rel="noopener noreferrer"&gt;upgrade to a paid account&lt;/a&gt; in the future if you need more search capacity, faster speeds, or other features.&lt;/p&gt;

&lt;p&gt;First, &lt;a href="https://serpapi.com/users/sign_up" rel="noopener noreferrer"&gt;create an account&lt;/a&gt; and verify your email and other details. After that, get the API key from &lt;a href="https://serpapi.com/manage-api-key" rel="noopener noreferrer"&gt;your account dashboard&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvpshn2l4ktvugjaqebqp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvpshn2l4ktvugjaqebqp.png" alt="The API key in the dashboard" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should &lt;a href="https://serpapi.com/blog/how-to-securely-store-api-keys/" rel="noopener noreferrer"&gt;store your API key in a safe location&lt;/a&gt; if you are sharing or publishing your code. If the key is leaked or stolen, you can make a new one from the SerpApi account dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install the SerpApi library (optional)
&lt;/h3&gt;

&lt;p&gt;You can use &lt;a href="https://serpapi.com/integrations" rel="noopener noreferrer"&gt;SerpApi's official libraries&lt;/a&gt; for Python, JavaScript, Ruby, Java, and other languages. They aren't required, but they provide a simple wrapper around the API requests.&lt;/p&gt;

&lt;p&gt;The APIs can also be used with cURL, &lt;code&gt;fetch()&lt;/code&gt; in Node.js, or anything else that can send GET requests and process a JSON response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract API with a GET request
&lt;/h3&gt;

&lt;p&gt;Here's how you could fetch the Hacker News home page with the Extract API, using a simple GET request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://serpapi.com/search.json?engine=extract&amp;amp;url=https://news.ycombinator.com&amp;amp;api_key=YOUR_KEY_GOES_HERE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default response is in JSON format, with basic page information in the &lt;code&gt;page_information&lt;/code&gt; object, a list of all included links in the &lt;code&gt;inline_links&lt;/code&gt; object, and so on.&lt;/p&gt;

&lt;p&gt;If you want the response in Markdown format, instead of JSON, just add &lt;code&gt;output=md&lt;/code&gt; to the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://serpapi.com/search.json?engine=extract&amp;amp;url=https://news.ycombinator.com&amp;amp;output=md&amp;amp;api_key=YOUR_KEY_GOES_HERE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need the page's rendered HTML, for building your own parser or some other use case, you can set &lt;code&gt;output=html&lt;/code&gt; for the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://serpapi.com/search.json?engine=extract&amp;amp;url=https://news.ycombinator.com&amp;amp;output=html&amp;amp;api_key=YOUR_KEY_GOES_HERE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Extract API with JavaScript
&lt;/h3&gt;

&lt;p&gt;Here's how you can use the Extract API with the &lt;a href="https://serpapi.com/integrations/javascript" rel="noopener noreferrer"&gt;SerpApi JavaScript library&lt;/a&gt; to display all posts from the first Hacker News home page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getJson&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serpapi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJson&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extract&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;YOUR_KEY_GOES_HERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://news.ycombinator.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scrape&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="k"&gt;of &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;inline_links&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Skip internal links, and the "API" link at the bottom of the page&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internal&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/HackerNews/API&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Display the title, link, and visible link text&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Title: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nLink: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nVisible Link: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayed_link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n----`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Extract API fetches the page contents and parses it, so all that is required is going through the list of extracted links (&lt;code&gt;search.inline_links&lt;/code&gt;) and filtering the results to external URLs.&lt;/p&gt;

&lt;p&gt;Here's a more advanced version that scans the first three pages, and saves the results to a CSV file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getJson&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;serpapi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://news.ycombinator.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;csvText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`"Title","Link","Visible Link"`&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;settings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extract&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;YOUR_KEY_GOES_HERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scrape&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/?p=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="k"&gt;of &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;inline_links&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Skip internal links, and the "API" link at the bottom of the page&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;internal&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/HackerNews/API&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Add the data as a new row&lt;/span&gt;
    &lt;span class="nx"&gt;csvText&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;`\n"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayed_link&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data.csv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;csvText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The crawl mode is helpful for analyzing many pages in a group. This example will crawl the UniFi store, up to 50 pages, to find every product page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// import { getJson } from 'serpapi';&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJson&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extract&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;YOUR_KEY_GOES_HERE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://store.ui.com/us/en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crawl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max_pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;productUrls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="c1"&gt;// Iterate through all crawled pages&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Iterate through all inline links&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="k"&gt;of &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;inline_links&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Filter out non-product links, and links already added to the list&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/products/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;productUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;productUrls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// List all products&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;productUrls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  More information
&lt;/h3&gt;

&lt;p&gt;You can learn more from the documentation pages for the &lt;a href="https://serpapi.com/extract-api" rel="noopener noreferrer"&gt;Extract API&lt;/a&gt;, &lt;a href="https://serpapi.com/extract-map-mode" rel="noopener noreferrer"&gt;Map mode&lt;/a&gt;, and &lt;a href="https://serpapi.com/extract-crawl-mode" rel="noopener noreferrer"&gt;Crawl mode&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>programming</category>
      <category>tutorial</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>7 Best Rank Tracking API Options Compared</title>
      <dc:creator>Noraina Nordin</dc:creator>
      <pubDate>Thu, 15 Oct 2026 09:11:51 +0000</pubDate>
      <link>https://dev.to/serpapi/7-best-rank-tracking-api-options-compared-4474</link>
      <guid>https://dev.to/serpapi/7-best-rank-tracking-api-options-compared-4474</guid>
      <description>&lt;p&gt;Once you track more than a few dozen keywords, checking positions by hand stops working. You need data you can load into a dashboard, a client report or your own app. Picking the best rank tracking API starts with one decision. You can buy a rank tracker that has an API, like Semrush, AccuRanker or Ahrefs. Or you can build on a SERP API, which returns the raw results page and lets you work out positions yourself.&lt;/p&gt;

&lt;p&gt;Rank trackers give you dashboards, history and alerts out of the box. SERP APIs give you more control over location, device and schedule, and usually cost less per keyword check. This guide compares 7 options across both camps on cost, accuracy, mobile support and AI Overview parsing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚦 &lt;em&gt;Disclosure: this is SerpApi's blog, and SerpApi is one of the 7 options. We list its limitations alongside everyone else's.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Best rank tracking APIs at a glance
&lt;/h2&gt;

&lt;p&gt;Prices and features below were checked on each provider's pricing and documentation pages in October 2026.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Provider&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Type&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Starting price&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Free tier&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Location &amp;amp; device targeting&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Update frequency&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;AI Overview parsing&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SerpApi&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;SERP API&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$25/mo for 1,000 searches&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;250 searches/mo&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;City, coordinates or &lt;code&gt;uule&lt;/code&gt;; desktop, tablet, mobile&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Real time, on your schedule&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Yes&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;DataForSEO&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;SERP API&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$0.60 per 1,000 SERPs (standard queue)&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$1 credit + sandbox&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Location code, name or coordinates; desktop, mobile&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;About 5 min (standard) to about 6 sec (live)&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Yes&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;Serpent API&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;SERP API&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$0.60 per 1,000 calls&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Free calls on sign-up&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Country and language only&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Real time, on your schedule&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Yes (Deep Search)&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;cloro&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;SERP API&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$30/mo for 37,500 credits&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;500 credits/mo&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Country, city or &lt;code&gt;uule&lt;/code&gt;; desktop, iOS, Android&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Real time, on your schedule&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Yes (+2 credits)&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;Semrush Position Tracking&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Rank tracker API&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$549/mo (Advanced) + API units&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;No API access on free plan&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Down to postal code; desktop, phone, tablet&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Daily&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Filter by AI Overview&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;AccuRanker&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Rank tracker API&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$764/mo (Expert, for API)&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Not stated&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Country or local; device not stated&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Daily + on-demand refresh&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Not stated&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;Ahrefs Rank Tracker&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Rank tracker API&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$129/mo (Lite)&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Not stated&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Country, state or city; desktop, mobile&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Weekly&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Yes&lt;/p&gt;

&lt;p&gt;|&lt;/p&gt;

&lt;p&gt;Pick by what you need most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SerpApi&lt;/strong&gt; if you want real-time results, city-level and tablet targeting, and no charge for failed or cached searches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DataForSEO&lt;/strong&gt; if cost per keyword matters more than speed and you can work with a task queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serpent API&lt;/strong&gt; if you only track country-level desktop rankings and want the lowest entry cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cloro&lt;/strong&gt; if AI Overview tracking is a core need and you want iOS and Android as separate devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semrush&lt;/strong&gt; if your team already works in Semrush and wants rankings flowing into its reports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AccuRanker&lt;/strong&gt; if you need on-demand refreshes and an unlimited read API at enterprise scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ahrefs&lt;/strong&gt; if weekly data is enough and you already pay for Ahrefs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  SERP API or rank tracker API?
&lt;/h2&gt;

&lt;p&gt;Both return a position for a keyword. The difference is who does the work between the search and the number.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;rank tracker API&lt;/strong&gt; sits on top of a finished product. You set up a project in Semrush, AccuRanker or Ahrefs, add keywords, and the tool checks them on its own schedule. The API then lets you read that stored history. You get clean data and no parsing. You also accept the tool's update frequency, its locations and its price per tracked keyword.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;SERP API&lt;/strong&gt; returns the full results page as JSON every time you call it. You decide when to check, from where and on which device. You then find your domain in the results and store the position yourself. That takes some code, but it removes per-seat pricing. It also lets you track things a dashboard may not show, like who holds the featured snippet.&lt;/p&gt;

&lt;p&gt;A rough rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose a rank tracker API if you need reports for non-technical stakeholders and your keyword list fits the plan.&lt;/li&gt;
&lt;li&gt;Choose a SERP API if you track many locations or devices, need checks on your own schedule, or want rankings inside your own product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://www.reddit.com/r/SEO_LLM/comments/1uwi5ob/seo_api_discussion_which_provider_works_best_for/" rel="noopener noreferrer"&gt;r/SEO_LLM discussion on SEO API providers&lt;/a&gt; shows the same split. Some people want finished metrics, while others want raw SERP data they can shape themselves.&lt;/p&gt;

&lt;p&gt;There is also a middle path. Open source trackers give you a dashboard but run on a SERP API you pay for directly. &lt;a href="https://github.com/towfiqi/serpbear" rel="noopener noreferrer"&gt;SerpBear&lt;/a&gt; is the best known. It is MIT licensed, runs in Docker, and can use SerpApi or several other scrapers.&lt;/p&gt;

&lt;p&gt;We built our own take on this idea. &lt;a href="https://serpapi.com/blog/introducing-serptrail-self-hosted-search-rank-tracker/" rel="noopener noreferrer"&gt;SerpTrail, our self-hosted rank tracker&lt;/a&gt;, runs in Docker with Rails and SQLite. It tracks Google rankings and AI Overviews across multiple locations on a daily, weekly, biweekly or monthly schedule. It can check up to five pages of Google results, and the &lt;a href="https://github.com/serpapi/serptrail" rel="noopener noreferrer"&gt;source code is on GitHub&lt;/a&gt;. It runs on your SerpApi key, so the free plan's 250 searches are enough to try it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10xjvs82enp3xhz6dy2y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10xjvs82enp3xhz6dy2y.png" alt="SerpTrail self-hosted rank tracker dashboard" width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SerpTrail self-hosted rank tracker dashboard&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes an accurate rank tracker?
&lt;/h2&gt;

&lt;p&gt;An accurate rank tracker is one that reports the results page your customers actually see. There is no single "true" position for a keyword. Google builds each page from the searcher's location, device, language and the moment of the search. A tracker is accurate when it controls all four and keeps them the same from one check to the next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Location, device, language and time
&lt;/h3&gt;

&lt;p&gt;Location matters most for anything with local intent. A national check and a check from a specific city can return different pages, so set the location at city level when it matters. If you target several countries, our guide to &lt;a href="https://serpapi.com/blog/global-seo-strategies-for-international-success" rel="noopener noreferrer"&gt;global SEO strategies&lt;/a&gt; covers how results shift by market and language. Match the language (&lt;code&gt;hl&lt;/code&gt;) and country (&lt;code&gt;gl&lt;/code&gt;) to your audience too.&lt;/p&gt;

&lt;p&gt;Device is the next big factor. On October 6, 2026, we ran "best running shoes for women" through SerpApi from Austin, Texas, once on desktop and once on mobile. Desktop returned 10 organic results and mobile returned 9. Only 6 pages appeared in both. Outside Online ranked #3 on desktop but #7 on mobile, and GearJunkie's #5 desktop result didn't appear on mobile at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsj0m6b7z0ad8081hf1b1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsj0m6b7z0ad8081hf1b1.png" alt="Desktop vs mobile Google organic results for the same keyword and location" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Desktop vs mobile Google organic results for the same keyword and location&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Time of check matters because rankings move during the day. Check at the same hour each day so changes reflect Google, not your schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why tools and humans see different results
&lt;/h3&gt;

&lt;p&gt;A common complaint is that a tool's ranking doesn't match a manual search. Usually the tool isn't wrong. This is caused by the person searching being logged in, having search history, or sitting a few miles from the location the tool uses. Accuracy comes from consistent parameters more than from any one provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  The num=100 change
&lt;/h3&gt;

&lt;p&gt;In September 2025, Google stopped supporting the &lt;code&gt;num=100&lt;/code&gt; parameter, which many tools used to &lt;a href="https://www.optimizely.com/field-notes/articles/googles-num100-parameter-is-gone" rel="noopener noreferrer"&gt;fetch 100 results in one request&lt;/a&gt;. Tracking the top 100 now takes about 10 paginated requests per keyword. Ask any provider how it handles depth and how it bills for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check against Search Console and pixel position
&lt;/h3&gt;

&lt;p&gt;Google Search Console is a useful sanity check. Its average position blends every location and device, so it won't match a single check. But a large, lasting gap between the two is a sign to recheck your tracker's settings.&lt;/p&gt;

&lt;p&gt;Position number alone also hides how visible a result is. A #1 organic result can sit below an AI Overview, ads and a local pack. &lt;a href="https://serpapi.com/blog/what-is-pixel-ranking-2/" rel="noopener noreferrer"&gt;Pixel ranking&lt;/a&gt; measures how far down the page a result appears, which tells you whether it is seen without scrolling. For more on how AI Overviews changed what a ranking means, read &lt;a href="https://serpapi.com/blog/rank-tracking-in-the-age-of-ai-overviews-whats-changed/" rel="noopener noreferrer"&gt;rank tracking in the age of AI Overviews&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72ksyh0fs94oerj2hy9h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72ksyh0fs94oerj2hy9h.png" alt="Organic result #1 pushed below the fold by an AI Overview" width="800" height="523"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Organic result #1 pushed below the fold by an AI Overview&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile rank tracking through an API
&lt;/h2&gt;

&lt;p&gt;Google uses mobile-first indexing, which means it &lt;a href="https://developers.google.com/search/docs/crawling-indexing/mobile/mobile-sites-mobile-first-indexing" rel="noopener noreferrer"&gt;uses the mobile version of a page for indexing and ranking&lt;/a&gt;. That doesn't mean desktop and mobile results are the same. The mobile page has a narrower layout, shows SERP features in a different order, and often places local packs higher. As our Austin test showed, the set of pages can differ as well as their order.&lt;/p&gt;

&lt;p&gt;For most sites, a large share of searches happen on phones. If you only track desktop, you may be looking at rankings that most of your visitors never see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile doubles the call count
&lt;/h3&gt;

&lt;p&gt;Every device is a separate search. Tracking 500 keywords daily on desktop and mobile means 1,000 checks a day, not 500. With a SERP API, that doubles your bill. Rank trackers handle it differently. Some count a mobile keyword as a separate tracked keyword, while others include both devices. Ahrefs, for example, says &lt;a href="https://help.ahrefs.com/en/articles/2085043-how-to-see-your-mobile-rankings-on-rank-tracker" rel="noopener noreferrer"&gt;mobile rankings are available on all Rank Tracker plans&lt;/a&gt;. Check this before comparing prices.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to check before you pick a provider
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A device parameter.&lt;/strong&gt; The API should accept a mobile device value. Some APIs have separate options for iOS and Android tracking, and a tablet setting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location depth on mobile.&lt;/strong&gt; City-level targeting matters even more on mobile, where local results take up more of the screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SERP features in the mobile response.&lt;/strong&gt; Make sure local packs, AI Overviews and other features are parsed for mobile, not only desktop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How mobile is billed.&lt;/strong&gt; Find out whether a mobile check costs the same as desktop or counts against a separate limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the seven APIs in this guide, Serpent API, has no device parameter in its documentation. That rules it out for mobile rank tracking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 7 best rank tracking APIs
&lt;/h2&gt;

&lt;p&gt;We start with the four SERP APIs, then cover the three rank tracker APIs. For each one, we cover targeting, data freshness, AI Overview support, cost and where it falls short.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. SerpApi
&lt;/h3&gt;

&lt;p&gt;SerpApi is a real-time SERP API. Each request runs a live Google search and returns the parsed results page as JSON or Markdown. It also covers Bing, YouTube, Google Maps and dozens of other engines through the same key.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; SERP API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; teams that need precise location and device control, or that want rankings inside their own product&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location and device targeting:&lt;/strong&gt; &lt;code&gt;location&lt;/code&gt; at city level, &lt;code&gt;uule&lt;/code&gt;, or &lt;code&gt;lat&lt;/code&gt; and &lt;code&gt;lon&lt;/code&gt; with a &lt;code&gt;radius&lt;/code&gt;. Set &lt;code&gt;gl&lt;/code&gt;, &lt;code&gt;hl&lt;/code&gt; and &lt;code&gt;google_domain&lt;/code&gt; for country and language. &lt;code&gt;device&lt;/code&gt; accepts desktop, tablet or mobile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh and scheduling:&lt;/strong&gt; real-time on every call, on whatever schedule you set. Results are cached for an hour, and cached searches are free. Add &lt;code&gt;no_cache&lt;/code&gt; to force a fresh search. &lt;a href="https://serpapi.com/blog/introducing-serptrail-self-hosted-search-rank-tracker/" rel="noopener noreferrer"&gt;SerpTrail&lt;/a&gt; adds daily, weekly, biweekly or monthly scheduling if you don't want to write your own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Overview parsing:&lt;/strong&gt; returned in an &lt;code&gt;ai_overview&lt;/code&gt; block. Sometimes Google loads it separately, and the response includes a &lt;code&gt;page_token&lt;/code&gt; you pass to the Google AI Overview API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting price and free tier:&lt;/strong&gt; 250 free searches a month. Paid plans start at $25 for 1,000 searches, then $75 for 5,000 and $150 for 15,000. Only successful searches count, so errors and failed searches are free. See every plan on the &lt;a href="https://serpapi.com/pricing" rel="noopener noreferrer"&gt;SerpApi pricing page&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0o0q8479ehf8y8ppsef.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft0o0q8479ehf8y8ppsef.png" alt="SerpApi Google Search Playground" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SerpApi Google Search Playground&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few extras matter for rank tracking. The Pixel Position API (&lt;code&gt;output=json_with_pixel_position&lt;/code&gt;) returns the on-page coordinates of each result, so you can measure pixel rank as well as position. Production plans and above include the U.S. Legal Shield, which covers legal questions around scraping public search results. SerpApi also offers an MCP server, so AI assistants can run searches directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; SerpApi costs more per search than queue-based APIs. At 30,000 searches a month, it is $275 against $18 on DataForSEO's standard queue. There is also no way to fetch 100 results in one request. You page through with &lt;code&gt;start&lt;/code&gt;, and each page is one search.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. DataForSEO
&lt;/h3&gt;

&lt;p&gt;DataForSEO sells SERP data by the request, with three speed tiers. You post a task and collect results later, or pay more to get them live.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; SERP API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; high-volume tracking where cost per keyword matters more than speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location and device targeting:&lt;/strong&gt; &lt;code&gt;location_code&lt;/code&gt;, &lt;code&gt;location_name&lt;/code&gt; or &lt;code&gt;location_coordinate&lt;/code&gt; (latitude, longitude and radius). &lt;code&gt;device&lt;/code&gt; is desktop or mobile, and &lt;code&gt;os&lt;/code&gt; sets Windows, macOS, Android or iOS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh and scheduling:&lt;/strong&gt; the standard queue returns in about 5 minutes. Live mode returns in up to about 6 seconds. &lt;code&gt;depth&lt;/code&gt; goes up to 700 results, billed per page of 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Overview parsing:&lt;/strong&gt; returned as an &lt;code&gt;ai_overview&lt;/code&gt; item. Setting &lt;code&gt;load_async_ai_overview&lt;/code&gt; fetches ones Google loads separately, for an extra $0.0006 per task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting price and free tier:&lt;/strong&gt; $0.0006 per SERP on the standard queue, $0.0012 on priority and $0.002 live. New accounts get $1 of credit and a free sandbox.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DataForSEO is the cheapest per search of the SERP APIs here. If you track tens of thousands of keywords a day and can wait a few minutes, that gap adds up quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the cheapest tier uses a post-then-fetch workflow, which means more code than a single synchronous call. There is no tablet device option.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Serpent API
&lt;/h3&gt;

&lt;p&gt;Serpent API is a low-cost SERP API that covers Google, Bing, Yahoo, DuckDuckGo and Brave. It is pay as you go, with discounts for larger deposits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; SERP API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; country-level desktop tracking on a tight budget, or tracking several search engines at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location and device targeting:&lt;/strong&gt; &lt;code&gt;country&lt;/code&gt; and &lt;code&gt;language&lt;/code&gt; only. The docs list no device or city parameter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh and scheduling:&lt;/strong&gt; real time on each call. &lt;code&gt;num&lt;/code&gt; accepts 1 to 100 and &lt;code&gt;pages&lt;/code&gt; accepts 1 to 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Overview parsing:&lt;/strong&gt; &lt;code&gt;include_aio&lt;/code&gt; on Deep Search requests, plus &lt;code&gt;include_ai_mode&lt;/code&gt; for AI Mode. A paid &lt;code&gt;pixel_position&lt;/code&gt; option is also available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting price and free tier:&lt;/strong&gt; $0.60 per 1,000 calls pay as you go, with a $10 minimum deposit. A $100 deposit unlocks 10× lower pricing and $500 unlocks 20× lower pricing. New accounts get free calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Serpent's own blog puts 1,000 keywords tracked daily at about $18 a month on the default tier. Paid credits never expire, which suits irregular usage. Promotional credits expire after 30 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; with no device or city targeting, Serpent API can't do mobile rank tracking or local rank tracking. It fits national desktop tracking only.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. cloro
&lt;/h3&gt;

&lt;p&gt;cloro is a SERP API with a credit system and a focus on AI search results, including AI Overviews.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; SERP API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; tracking AI Overview presence next to classic rankings at a mid-range price&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location and device targeting:&lt;/strong&gt; &lt;code&gt;country&lt;/code&gt; (or &lt;code&gt;gl&lt;/code&gt;), plus &lt;code&gt;location&lt;/code&gt; by canonical name or &lt;code&gt;uule&lt;/code&gt; for city level. &lt;code&gt;hl&lt;/code&gt; sets language. &lt;code&gt;device&lt;/code&gt; accepts desktop, ios, android or mobile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh and scheduling:&lt;/strong&gt; real time on each call. &lt;code&gt;pages&lt;/code&gt; fetches 1 to 10 pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Overview parsing:&lt;/strong&gt; set &lt;code&gt;include.aioverview&lt;/code&gt; for an extra 2 credits per search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting price and free tier:&lt;/strong&gt; 500 free credits a month. Lite is $30 for 37,500 credits and Hobby is $100 for 250,000. Higher tiers bring the rate down to $0.31 per 1,000 credits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The math for search credits is a bit complicated here. A Google search costs 3 credits, each extra page adds 2, and AI Overview adds 2. Synchronous requests add 2 more. A synchronous search with AI Overview therefore costs 7 credits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the headline price per 1,000 credits is not a price per search. Unused credits also don't roll over to the next month.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Semrush Position Tracking
&lt;/h3&gt;

&lt;p&gt;Semrush Position Tracking is a full rank tracker inside the Semrush suite. Its API lets you create campaigns, add keywords and pull ranking reports into your own tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; rank tracker API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; agencies and in-house teams already reporting from Semrush&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location and device targeting:&lt;/strong&gt; locations down to postal code. Devices are desktop, phone and tablet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh and scheduling:&lt;/strong&gt; daily updates, tracking up to the top 100 positions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Overview parsing:&lt;/strong&gt; you can filter tracked keywords by SERP features, including AI Overview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting price and free tier:&lt;/strong&gt; API access starts on the Advanced plan at $549 a month, billed monthly. There is no API access on the free plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;API calls use Semrush API units on top of the subscription. Creating a campaign, adding a keyword and requesting a report each cost 100 units. Check how many units your plan includes before you plan a large integration.&lt;/p&gt;

&lt;p&gt;Semrush's strength is everything around the rankings. Keyword volumes, competitor visibility and site audits live in the same account, so the ranking data connects to the rest of your SEO work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the price of entry is high if you only need ranking data. API units are a second cost to track, and you are tied to Semrush's daily schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. AccuRanker
&lt;/h3&gt;

&lt;p&gt;AccuRanker is a dedicated rank tracker known for speed and on-demand refreshes. It is built for agencies and large in-house teams.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; rank tracker API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; large keyword sets that need fresh data on demand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location and device targeting:&lt;/strong&gt; country or local level. The pages we checked don't state device options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh and scheduling:&lt;/strong&gt; daily by default. You can also refresh one keyword or a whole domain on demand, every 2 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Overview parsing:&lt;/strong&gt; not stated on the pages we checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting price and free tier:&lt;/strong&gt; an unlimited read API comes with the Expert plan at $764 a month. The write API, for adding keywords and domains, needs an Enterprise plan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AccuRanker's features page describes full API access at no additional cost. That makes the read API good value if you already need Expert-level tracking. You can pull as much history as you like without counting units.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the API sits behind a high-priced plan, and creating projects by API needs Enterprise. Confirm mobile tracking and AI Overview support with sales before you buy.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Ahrefs Rank Tracker
&lt;/h3&gt;

&lt;p&gt;Ahrefs Rank Tracker is part of the Ahrefs toolset. Its API returns stored rankings for the projects you set up in Ahrefs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; rank tracker API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best for:&lt;/strong&gt; teams already on Ahrefs who can work with weekly data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location and device targeting:&lt;/strong&gt; country, state or province, or city. Each request sets &lt;code&gt;device&lt;/code&gt; to desktop or mobile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refresh and scheduling:&lt;/strong&gt; weekly updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Overview parsing:&lt;/strong&gt; responses list SERP features, including &lt;code&gt;ai_overview&lt;/code&gt;. A &lt;code&gt;best_position_kind&lt;/code&gt; field shows when your best position is inside an AI Overview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starting price and free tier:&lt;/strong&gt; Lite costs $129 a month and includes 750 tracked keywords. It also includes API and MCP access with 1,000 credits per user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rank-tracker-overview endpoint needs a project ID, a device and a date. It returns position, URL, location and SERP features for each keyword. Ahrefs also offers an MCP server, so you can query rankings from an AI assistant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; weekly data misses short-term swings, such as the days after an algorithm update. The 1,000 API credits on Lite also limit how often you can pull data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What rank tracking costs per keyword
&lt;/h2&gt;

&lt;p&gt;SERP APIs charge per search, so the cost of tracking comes down to one formula:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monthly searches = keywords × devices × locations × checks per month&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Take a common setup: 500 keywords, checked on desktop and mobile, from one location, every day. That is 500 × 2 × 1 × 30, or 30,000 searches a month. Here is what that costs with each provider.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Provider&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;What 30,000 checks needs&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Monthly cost&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Notes&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;SerpApi&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;30,000 searches&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$275 (Big Data plan)&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Real time; failed and cached searches are free&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;DataForSEO&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;30,000 SERPs&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$18 standard, $36 priority, $60 live&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Standard queue takes about 5 minutes&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;Serpent API&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;15,000 calls (desktop only)&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;About $9&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;No mobile option, so half the setup isn't possible&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;cloro&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;90,000 to 210,000 credits&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$100 (Hobby plan)&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Range depends on AI Overview and sync requests&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;Semrush&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;500 keywords in Position Tracking&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$549 + API units&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Advanced plan needed for API&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;AccuRanker&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;500 keywords&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$764 (Expert plan)&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Unlimited read API&lt;/p&gt;

&lt;p&gt;|&lt;br&gt;
| &lt;/p&gt;

&lt;p&gt;Ahrefs&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;500 keywords&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;$129 (Lite plan)&lt;/p&gt;

&lt;p&gt;| &lt;/p&gt;

&lt;p&gt;Weekly updates only, so not a like-for-like match&lt;/p&gt;

&lt;p&gt;|&lt;/p&gt;

&lt;p&gt;Three things change the math. Each extra location adds another full set of searches. Tracking beyond the top 10 multiplies SERP API costs, since the end of &lt;code&gt;num=100&lt;/code&gt; means each extra page is a separate request. And rank trackers bill by tracked keyword, so their price stays flat however often they check.&lt;/p&gt;

&lt;p&gt;For small keyword lists, a rank tracker can be the simpler buy. Once you track many locations or devices, a SERP API is usually cheaper per check.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the best rank tracker tool?
&lt;/h3&gt;

&lt;p&gt;It depends on how you'll use the data. For dashboards and client reports, a rank tracker such as Semrush, AccuRanker or Ahrefs is the simplest choice. For tracking inside your own app, or across many locations and devices, use a SERP API like SerpApi or DataForSEO. You get more control at a lower cost per check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which AI-powered rank tracker is the best?
&lt;/h3&gt;

&lt;p&gt;Most tools now report AI Overviews in some form. Ahrefs and Semrush flag when one appears for a tracked keyword. SERP APIs like SerpApi, DataForSEO and cloro return the AI Overview text and its cited sources, so you can see whether your site is cited. To try this yourself, follow our guide to &lt;a href="https://serpapi.com/blog/ai-overview-rank-tracker-using-python/" rel="noopener noreferrer"&gt;track AI Overview rankings in Python&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I check my ranking?
&lt;/h3&gt;

&lt;p&gt;For a one-off check, search in a private browser window with location set to your target city. Then compare with the average position in Google Search Console. For ongoing tracking, use a rank tracker or a SERP API so every check uses the same location, device and language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start tracking your rankings
&lt;/h2&gt;

&lt;p&gt;SerpApi gives you real-time Google results with city-level and device targeting, AI Overview parsing and pixel positions. Failed searches are never billed. &lt;a href="https://serpapi.com/users/sign_up" rel="noopener noreferrer"&gt;Sign up for a free account&lt;/a&gt; to get 250 free searches a month, then try your first ranking check in minutes.&lt;/p&gt;

&lt;p&gt;If you need help getting started, &lt;a href="https://serpapi.com/#contact" rel="noopener noreferrer"&gt;contact us&lt;/a&gt;. We're happy to help.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>What Developers Built With SerpApi at API World 2026</title>
      <dc:creator>Sonika Arora</dc:creator>
      <pubDate>Mon, 12 Oct 2026 21:43:46 +0000</pubDate>
      <link>https://dev.to/serpapi/what-developers-built-with-serpapi-at-api-world-2026-nj2</link>
      <guid>https://dev.to/serpapi/what-developers-built-with-serpapi-at-api-world-2026-nj2</guid>
      <description>&lt;p&gt;At API World 2026, we wanted developers to do more than just hear about SerpApi. So we put our APIs in their hands and let them build.&lt;/p&gt;

&lt;p&gt;At our booth, developers could experiment with SerpApi live, connect search data to their existing tools and workflows, and see what they could create with real-time web data. Many developers participated in our hackathon and used SerpApi to build real-time search capabilities, automated workflows, and AI-driven applications.&lt;/p&gt;

&lt;p&gt;The result? A collection of 124 amazing projects that showed just how many ways developers can put search data to work.&lt;/p&gt;

&lt;p&gt;Check out five of the many interesting projects built with SerpApi at API World.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. AccessForm
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Call. Talk. Your form is filled.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The people who most need help with official paperwork are the ones least able to do it online. A blind person with a hospital bill, a 70-year-old who can't walk to the bus, a student with ADHD facing a disability office form. They all have a phone. Most of them don't have a screen they can use, and some don't have internet at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  How AccessForm helps
&lt;/h3&gt;

&lt;p&gt;You call a number and say what's going on in your own words. AccessForm works out which official program fits, finds the real application form from the official source, asks you its questions one at a time in plain language, fills the actual PDF, and texts you the result with a list of what's still missing.&lt;/p&gt;

&lt;p&gt;Everything happens in one phone call. It never submits, never signs, and never decides eligibility. If it can't verify an official form for the exact organization you named, it says so and stops.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/embed/F-DBDH3wa48?feature=oembed" rel="noopener noreferrer"&gt;https://www.youtube.com/embed/F-DBDH3wa48?feature=oembed&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How SerpApi was used
&lt;/h3&gt;

&lt;p&gt;Vapi runs the voice agent on a Twilio number. Every tool call hits a Next.js backend that uses SerpApi to find and verify the official form, OpenAI to read the form's fields and turn them into spoken questions, pdf-lib to fill the real PDF, Nutrient for accessibility tagging when credits allow, and Xano as the system of record for cases, answers, and completeness. A conversation page rebuilds the call live from Xano's event stream, so a laptop can watch a phone call as it happens.&lt;/p&gt;

&lt;p&gt;The developers used SerpApi to search for and discover official application forms (PDFs) based on a user's specified need, organization, and location (e.g., executing live web queries to find official &lt;code&gt;.gov&lt;/code&gt;, &lt;code&gt;.edu&lt;/code&gt;, or official organizational site links).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built with:&lt;/strong&gt; SerpApi's &lt;a href="https://serpapi.com/search-api" rel="noopener noreferrer"&gt;Google Search API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://devpost.com/software/accessform" rel="noopener noreferrer"&gt;&lt;strong&gt;See project details&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/ali-amjad52114/accessform" rel="noopener noreferrer"&gt;&lt;strong&gt;Github&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. PantryProof
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PantryProof turns fragmented recall evidence into cited, human-reviewed actions and a replayable closure packet for every partner pantry.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Food banks and partner pantries form the “true last mile” of a food recall. Products may arrive through central distribution, retail rescue, local purchasing, food drives, and other paths. Partner agencies may operate intermittently or rely on volunteers, so sending an alert does not prove that the affected product was found, contained, or communicated downstream.&lt;/p&gt;

&lt;p&gt;PantryProof was built around a simple question: How can a food bank prove that every partner completed the correct recall response without letting an uncertain AI prediction make the final safety decision?&lt;/p&gt;

&lt;h3&gt;
  
  
  How PantryProof helps
&lt;/h3&gt;

&lt;p&gt;PantryProof streamlines food safety responses across partner pantries by discovering official recall notices via SerpApi, extracting cited data, requiring human approval for extracted fields, and deterministically matching identifiers (UPC/GTIN and lot numbers) against inventory to trigger necessary quarantine, disposal, or notification actions before generating a verified, audit-ready PDF closure packet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://player.vimeo.com/video/1223743447?app_id=122963" rel="noopener noreferrer"&gt;https://player.vimeo.com/video/1223743447?app_id=122963&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The browser experience is built with TypeScript, JavaScript, semantic HTML, and responsive CSS. A deterministic TypeScript domain engine owns identifier normalization, matching policy, agency aggregation, human review state, response completeness, and closure readiness.&lt;/p&gt;

&lt;h3&gt;
  
  
  How SerpApi was used
&lt;/h3&gt;

&lt;p&gt;Developers used SerpApi to get structured, current Google News recall results ordered newest first. They limit results to FDA and USDA domains. PantryProof validates every returned URL, determines evidence tier from the actual hostname rather than an untrusted label, and allows a coordinator to attach a reviewed result as canonical evidence.&lt;/p&gt;

&lt;p&gt;This makes web search part of a traceable evidence workflow instead of treating the first search result as truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built with:&lt;/strong&gt; SerpApi's &lt;a href="https://serpapi.com/google-news-api" rel="noopener noreferrer"&gt;Google News API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://devpost.com/software/pantryproof" rel="noopener noreferrer"&gt;&lt;strong&gt;See project details&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/Anupam0202/pantryproof" rel="noopener noreferrer"&gt;&lt;strong&gt;Github&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://pantryproof.vercel.app/" rel="noopener noreferrer"&gt;&lt;strong&gt;See it live&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  3. ClinicalBrief AI (by LearnHive Labs)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Upload patient lab data - GPT-4o searches PubMed, retrieves clinical guidelines, and delivers a signed clinical brief to the clinician in under 60 seconds.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clinicians spend 2+ hours per complex patient case searching literature, cross-referencing lab values against treatment protocols, and writing structured briefs. Most existing AI tools either hallucinate clinical recommendations or cost $50,000+ per year. There is no open, inspectable, evidence-grounded pipeline that any clinic can run.&lt;/p&gt;

&lt;h3&gt;
  
  
  How ClinicalBrief AI helps
&lt;/h3&gt;

&lt;p&gt;ClinicalBrief AI was built based on a research publication on adaptive health orchestration to prove that a fully agentic, source-cited, confidence-gated clinical intelligence pipeline can be built on open infrastructure - in n8n, without a single line of server code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/embed/OMoF1RoaBQA?feature=oembed" rel="noopener noreferrer"&gt;https://www.youtube.com/embed/OMoF1RoaBQA?feature=oembed&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A patient or pharmacy staff member fills a Google Sheet with lab values: HbA1c, LDL, BMI, blood pressure, medications, diagnoses. No app. No login. No technical knowledge required.&lt;/p&gt;

&lt;h3&gt;
  
  
  How SerpApi was used
&lt;/h3&gt;

&lt;p&gt;When a new row is added, the n8n pipeline fires automatically - &lt;strong&gt;SerpApi&lt;/strong&gt; queries Google Scholar in real-time using the patient's diagnoses. Three current PubMed publications are retrieved and injected into the agent prompt before any generation happens. GPT-4o generates clinical briefs using patient data, PubMed evidence, and clinical guidelines. Briefs with a confidence score of 80% or higher are electronically signed and delivered to clinicians, while those below are reviewed by humans. All data is sent to a physician.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built with:&lt;/strong&gt; SerpApi's &lt;a href="https://serpapi.com/google-scholar-api" rel="noopener noreferrer"&gt;Google Scholar API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://devpost.com/software/clinicalbrief-ai" rel="noopener noreferrer"&gt;&lt;strong&gt;See project details&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/udaytx009/ClinicalbriefAI/tree/main" rel="noopener noreferrer"&gt;&lt;strong&gt;Github&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. ApplyPilot
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ApplyPilot finds real opportunities and proves why they match you - using grounded evidence, deterministic scoring, bounded AI, and explicit uncertainty instead of black-box recommendations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most AI job-matching systems give candidates a score. But a score can hide a dangerous assumption: when evidence is missing, an AI system may treat that missing information as if it were negative evidence. That idea became the foundation for an evidence-controlled opportunity intelligence system where AI can interpret evidence, but cannot silently manufacture it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How ApplyPilot helps
&lt;/h3&gt;

&lt;p&gt;ApplyPilot uses a multi-step process to match candidates with real job opportunities. It extracts evidence from résumés, verifies it, and then searches the web for relevant jobs. The final result is a ranked list of opportunities with detailed explanations of why they are a good fit.&lt;/p&gt;

&lt;p&gt;The end-to-end workflow is:&lt;/p&gt;

&lt;p&gt;Résumé → grounded evidence → human review → candidate profile → live web discovery → deterministic evaluation → bounded AI reasoning → self-audit → ranked opportunities → Why this job?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/embed/X8vQZmbpb20?feature=oembed" rel="noopener noreferrer"&gt;https://www.youtube.com/embed/X8vQZmbpb20?feature=oembed&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How SerpApi was used
&lt;/h3&gt;

&lt;p&gt;ApplyPilot uses SerpApi's Google Jobs API to discover real public opportunities during a Mission. The system preserves the provider query, raw listing evidence, canonical opportunity, and source lineage. Repostings can be deterministically deduplicated while preserving the underlying sources.&lt;/p&gt;

&lt;p&gt;SerpApi helps ApplyPilot operate on changing real-world opportunities rather than a static demonstration dataset. Live web data changes the AI experience directly: the reasoning pipeline evaluates opportunities discovered during the run instead of answering from stale or fabricated listings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built with:&lt;/strong&gt; SerpApi's &lt;a href="https://serpapi.com/google-jobs-api" rel="noopener noreferrer"&gt;Google Jobs API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://devpost.com/software/brandfence" rel="noopener noreferrer"&gt;&lt;strong&gt;See project details&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/Alyht/ApplyPilot" rel="noopener noreferrer"&gt;&lt;strong&gt;Github&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://applypilot-alyht.vercel.app/" rel="noopener noreferrer"&gt;&lt;strong&gt;See it live&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  5. DepositCheck
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DepositCheck protects renters from listing scams by using SerpApi's&lt;/strong&gt; &lt;a href="https://serpapi.com/google-lens-api" rel="noopener noreferrer"&gt;&lt;strong&gt;Google Lens API&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;to perform visual reverse image searches on listing photos, matching them against the live web to confirm if an apartment matches its claimed address or belongs to another property.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rental scams work by theft, not invention. A scammer copies the photos and the description from a real listing, swaps the contact details and the address, and reposts it somewhere with weaker verification. Someone wires a deposit for a property the "landlord" has never owned.&lt;/p&gt;

&lt;p&gt;The photos are the one thing the scammer cannot change. Swap them out and the listing stops looking like the flat that attracts victims in the first place. But a renter cannot check that. You would have to recognize one apartment interior among billions of published images, from memory, on a phone, while a "landlord" tells you three other people are viewing tonight.&lt;/p&gt;

&lt;h3&gt;
  
  
  How DepositCheck helps
&lt;/h3&gt;

&lt;p&gt;You upload one photo from the listing and type the address you were given. SerpApi's &lt;code&gt;google_lens&lt;/code&gt; engine with &lt;code&gt;type=exact_matches&lt;/code&gt; runs Google's visual matching model over the live web index and returns every page carrying that same image. The claimed address is matched against what those pages say. The photo is deleted as soon as the lookup returns.&lt;/p&gt;

&lt;p&gt;One of three verdicts is generated - CORROBORATED (a genuine syndicated listing), CONTRADICTED (the photos belong to one specific &lt;em&gt;other&lt;/em&gt; address), or UNVERIFIED (not enough evidence either way).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://player.vimeo.com/video/1223247077?app_id=122963" rel="noopener noreferrer"&gt;https://player.vimeo.com/video/1223247077?app_id=122963&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  How SerpApi was used
&lt;/h3&gt;

&lt;p&gt;DepositCheck uses SerpApi's Google Lens API with &lt;code&gt;type=exact_matches&lt;/code&gt; to return every page carrying that same image. The claimed address is matched against what those pages say. The team aims to extend the solution in the future by using Google Maps API to verify the address is a residential property rather than a parking lot or a mailbox storefront and SerpApi's Google Search API on the landlord's phone number and email, which scam operations reuse across dozens of listings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Built with:&lt;/strong&gt; SerpApi's &lt;a href="https://serpapi.com/google-lens-api" rel="noopener noreferrer"&gt;Google Lens API&lt;/a&gt;, &lt;a href="https://serpapi.com/google-maps-api" rel="noopener noreferrer"&gt;Google Maps API&lt;/a&gt; (Future Extension), and &lt;a href="https://serpapi.com/search-api" rel="noopener noreferrer"&gt;Google Search API&lt;/a&gt; (Future Extension)&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://devpost.com/software/depositcheck" rel="noopener noreferrer"&gt;&lt;strong&gt;See project details&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/ishal1410/depositcheck" rel="noopener noreferrer"&gt;&lt;strong&gt;Github&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://depositcheck-liart.vercel.app/" rel="noopener noreferrer"&gt;&lt;strong&gt;See it live&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Developers Are Building With Search Data
&lt;/h2&gt;

&lt;p&gt;The projects built at API World were very different from one another, but they had something in common:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Developers were using search as a building block for something bigger&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Some projects focused on AI and agents, using search results to give applications access to current information. Others used search data for automation, research, data collection, and discovery.&lt;/p&gt;

&lt;p&gt;And because SerpApi returns structured search results in JSON and Markdown, developers can take that data and plug it directly into the applications and workflows they're already building.&lt;/p&gt;

&lt;p&gt;Check out all the projects developers built with SerpApi at API World here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://api-cloud-ai-hackathon-2026.devpost.com/submissions/search?utf8=%E2%9C%93&amp;amp;prize_filter%5Bprizes%5D%5B%5D=102840" rel="noopener noreferrer"&gt;DevNetwork [API + Cloud + AI] Hackathon 2026&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The projects built at API World are just a small sample of what's possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build with SerpApi
&lt;/h2&gt;

&lt;p&gt;For us, the most exciting part wasn't simply seeing developers call an API and build a tool using it. It was seeing what they built once they had access to real search data.&lt;/p&gt;

&lt;p&gt;Whether you're building an AI agent, research tool, automation, data pipeline, or something completely different, live search results can be an important source of fresh information for your application.&lt;/p&gt;

&lt;p&gt;And you don't have to build the search infrastructure yourself.&lt;/p&gt;

&lt;p&gt;With SerpApi, developers can access Google and other search engines through an easy and fast API and work with the results in a format that's ready to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have an idea?&lt;/strong&gt; &lt;a href="https://serpapi.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Start building&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building something useful with SerpApi?&lt;/strong&gt; &lt;a href="https://serpapi.github.io/BuiltWithSerpApi/" rel="noopener noreferrer"&gt;&lt;strong&gt;Share it&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;with us.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interested in exploring our use cases? Check out our&lt;/strong&gt; &lt;a href="https://serpapi.com/use-cases" rel="noopener noreferrer"&gt;&lt;strong&gt;use cases page&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Read Some Of Our Latest Posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://serpapi.com/blog/introducing-apptrail-a-self-hosted-mobile-app-ranking-and-ai-visibility-tracker/" rel="noopener noreferrer"&gt;Introducing AppTrail: A self-hosted mobile app ranking and AI visibility tracker&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://serpapi.com/blog/how-to-scrape-tripadvisor-place-data/" rel="noopener noreferrer"&gt;How to Scrape Tripadvisor Place Data&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://serpapi.com/blog/amazon-keyword-research-python/" rel="noopener noreferrer"&gt;How to Do Amazon Keyword Research with Python&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Introducing Review Audit: An open-source tool to check how real a Google Maps rating is</title>
      <dc:creator>Cagdas Salur</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:37:48 +0000</pubDate>
      <link>https://dev.to/serpapi/introducing-review-audit-an-open-source-tool-to-check-how-real-a-google-maps-rating-is-2p65</link>
      <guid>https://dev.to/serpapi/introducing-review-audit-an-open-source-tool-to-check-how-real-a-google-maps-rating-is-2p65</guid>
      <description>&lt;p&gt;Review Audit reads a Google Maps place's newest reviews, pulls the full Google history of the reviewers worth a closer look, and tells you how much of the rating the place's own reviewers can explain. What they can't explain gets set aside, and you see the rating without it. It is open source, runs on your machine with your own SerpApi key, and costs about 42 credits a place.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/serpapi/reviewaudit" rel="noopener noreferrer"&gt;GitHub - serpapi/reviewaudit: Is this 4.8 real? Reads a Google Maps place’s newest reviews through SerpApi and shows the patterns worth a closer look: first-time reviewers, five-star bursts, reviews posted minutes apart. Runs locally on your own API key.&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What an audit looks like
&lt;/h2&gt;

&lt;p&gt;Each place you read gets one page: the verdict, the rating without the reviews it can't explain, and the evidence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F56wgtcwoyqb7rnxrqgvu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F56wgtcwoyqb7rnxrqgvu.png" alt="An audit of Gull Rock Dinner Cruise, an invented place from the demo" width="800" height="1094"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An audit of Gull Rock Dinner Cruise, an invented place from the demo&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every place in these screenshots is invented. Review Audit ships a demo that puts made-up businesses with made-up reviews on real Portland, Maine streets, so the images show the real tool without pointing at a real business. More on that at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you can't spot a fake review by reading it
&lt;/h2&gt;

&lt;p&gt;Every business with a rating has an incentive to improve it, and there is a market that will help. On the place page, a review that was paid for or asked for looks exactly like a real one: five stars, a name, a date.&lt;/p&gt;

&lt;p&gt;They don't look the same in aggregate. Padded reviews tend to come from accounts that have never reviewed anything else. They arrive together, in office hours, minutes apart. They say the same thing in the same words. None of that is visible from one review, and all of it is visible from 200 of them plus the history of the people who wrote them. That is what Review Audit reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks for
&lt;/h2&gt;

&lt;p&gt;Each pattern is measured against the place itself, not against a global rule. A busy lunch spot that gets 30 reviews on a Saturday is not suspicious; a dentist that gets 30 in a week when its normal week is four is.&lt;/p&gt;

&lt;h3&gt;
  
  
  First-time reviewers
&lt;/h3&gt;

&lt;p&gt;Reviewers with no record like it more than anyone else. Review Audit compares how often first-time accounts give five stars against accounts with 4 to 50 reviews, at the same place. The gap is the finding, not the count. A famous place gets plenty of first reviews; a place where first reviews are five stars far more often than everyone else's has something to explain.&lt;/p&gt;

&lt;h3&gt;
  
  
  A week with too many five-star reviews
&lt;/h3&gt;

&lt;p&gt;A week carried far more five-star reviews than the place gets. Judged against the place's own median week, and flagged only when it is both at least 2.5 times normal and very unlikely by chance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Five-star reviews minutes apart
&lt;/h3&gt;

&lt;p&gt;Five-star reviews arrive in batches, minutes apart. More pairs within ten minutes than the hours people actually post at would predict, so a lunch rush is not mistaken for a batch upload.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same waiter, over and over
&lt;/h3&gt;

&lt;p&gt;Reviews that name the same member of staff, posted in pairs after dinner by people with the same surname, are a different kind of padding: reviews asked for at the table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Repeated wording and shared reviewers
&lt;/h3&gt;

&lt;p&gt;Reviews that say the same thing in the same words, and groups of accounts that keep reviewing the same places. Two accounts that share other places are usually a couple. Review Audit needs three.&lt;/p&gt;

&lt;h3&gt;
  
  
  Almost no four-star reviews
&lt;/h3&gt;

&lt;p&gt;Nobody ever gives it four stars. Real places collect a four-star tail: the customer who enjoyed it but waited too long. A wall of fives with nothing beside it means someone is choosing who gets asked.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does with what it finds
&lt;/h3&gt;

&lt;p&gt;None of these condemns a single review on its own. What gets set aside is only the excess over what the place's own reviewers explain, and every review set aside is listed with a link back to Google, so you can disagree.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ckebum0qyeusj05g1pz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7ckebum0qyeusj05g1pz.png" alt="The audits so far, most padded first" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The audits so far, most padded first&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the reviews say
&lt;/h2&gt;

&lt;p&gt;Below the findings, each audit also reads the reviews for what they say: what people praise and what they complain about, food/service/atmosphere sub-ratings with and without the set-aside reviews, the hours people post at, where else the reviewers go, and how often the owner replies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3d6twv6rzt0urjd1duuj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3d6twv6rzt0urjd1duuj.png" alt="What the reviews say: praise, complaints, sub-ratings and the hours people post at" width="800" height="1601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What the reviews say: praise, complaints, sub-ratings and the hours people post at&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each audit is a single self-contained HTML file, so it can be kept, compared and shared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 5 minutes
&lt;/h2&gt;

&lt;p&gt;To install Review Audit, you can use &lt;code&gt;uv tool install&lt;/code&gt; command as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/serpapi/reviewaudit
reviewaudit serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That opens the app. Paste your key from &lt;a href="https://serpapi.com/manage-api-key" rel="noopener noreferrer"&gt;serpapi.com/manage-api-key&lt;/a&gt;, search for a place, and press &lt;strong&gt;Read the reviews&lt;/strong&gt;. The free plan's 250 searches a month read about five places.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysvmagbotwzclbj6azrx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fysvmagbotwzclbj6azrx.png" alt="Searching Google Maps from the app" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Searching Google Maps from the app&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The read runs in the background and shows each stage, so you can watch where the credits go:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkapjgk91s7qwsqcoornw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkapjgk91s7qwsqcoornw.png" alt="A read in progress, one stage at a time" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A read in progress, one stage at a time&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There is a CLI too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SERPAPI_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
reviewaudit &lt;span class="s2"&gt;"&amp;lt;place name&amp;gt; &amp;lt;city&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it uses SerpApi
&lt;/h2&gt;

&lt;p&gt;Three SerpApi engines, about 42 calls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;step&lt;/th&gt;
&lt;th&gt;engine&lt;/th&gt;
&lt;th&gt;what it gives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href="https://serpapi.com/google-maps-api" rel="noopener noreferrer"&gt;Google Maps API&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the place, its photo, hours and Google's lifetime star counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://serpapi.com/google-maps-reviews-api" rel="noopener noreferrer"&gt;Google Maps Reviews API&lt;/a&gt; ×10&lt;/td&gt;
&lt;td&gt;the 200 newest reviews, each with the reviewer's review count, Local Guide status, photos, sub-ratings and an exact timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://serpapi.com/google-maps-contributor-reviews-api" rel="noopener noreferrer"&gt;Google Maps Contributor Reviews API&lt;/a&gt; ×30&lt;/td&gt;
&lt;td&gt;everything the reviewers worth checking have ever reviewed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cheap checks run first, so the 30 paid record lookups go to the reviewers they point at. Every response is cached, so re-reading a place or rebuilding its page costs nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you build on these APIs yourself
&lt;/h3&gt;

&lt;p&gt;A few things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The contributor engine returns relative dates only ("a week ago"); the reviews engine has &lt;code&gt;iso_date&lt;/code&gt;. So timing patterns come from the place's reviews, and a reviewer's record is used for &lt;em&gt;what&lt;/em&gt; they reviewed, not when.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user.reviews&lt;/code&gt; on a review counts written reviews. The full record also has rating-only entries, so an account that says "1 review" can have 19 ratings behind it. Review Audit trusts the record.&lt;/li&gt;
&lt;li&gt;The first page of reviews is 8, the rest 20.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it can't tell you
&lt;/h2&gt;

&lt;p&gt;It reports patterns worth a closer look, not verdicts on individual reviews, which is why every verdict reads "looks". It also has blind spots, and each audit prints them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A place where everyone gives five stars looks the same whether they mean it or not. A flat 95% is not evidence; the gradient across account depth is.&lt;/li&gt;
&lt;li&gt;A first review at a famous place is normal. A thin account alone never counts.&lt;/li&gt;
&lt;li&gt;Hotels carry Tripadvisor and Trip.com reviews with no Google account behind them. Those are left out, and the page says how many.&lt;/li&gt;
&lt;li&gt;200 reviews at a very busy place can be two weeks. The audit says so when the window is short, and &lt;code&gt;--reviews 600&lt;/code&gt; reads further back.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About those screenshots
&lt;/h2&gt;

&lt;p&gt;Running a tool like this against real businesses and publishing the results is a different thing from building it. So the repo includes &lt;code&gt;tools/demo.py&lt;/code&gt;: a fake Google Maps that answers the same three engines in the same shapes, for businesses that don't exist (every name was checked against Google Maps) on real Portland streets, with invented reviews and reviewers. The padding is written in on purpose, a burst here, a waiter named over and over there, and the real Review Audit finds it. Every image in the &lt;code&gt;README&lt;/code&gt; file and in this post comes from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the code
&lt;/h2&gt;

&lt;p&gt;The code is on &lt;a href="https://github.com/serpapi/reviewaudit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Issues and pull requests are welcome, especially new patterns with a rationale behind them, and staff-name and stop-word lists for languages beyond English and Turkish.&lt;/p&gt;

&lt;p&gt;If you don't have a key yet, &lt;a href="https://serpapi.com/users/sign_up" rel="noopener noreferrer"&gt;the free plan&lt;/a&gt; is 250 searches a month, enough for about five places.&lt;/p&gt;

</description>
      <category>analysis</category>
      <category>github</category>
      <category>opensource</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
