<?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>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>
    <item>
      <title>Introducing SerpApi's OpenCode Plugin</title>
      <dc:creator>Srujana Maddula</dc:creator>
      <pubDate>Tue, 06 Oct 2026 10:03:36 +0000</pubDate>
      <link>https://dev.to/serpapi/introducing-serpapis-opencode-plugin-4j25</link>
      <guid>https://dev.to/serpapi/introducing-serpapis-opencode-plugin-4j25</guid>
      <description>&lt;p&gt;OpenCode is an open-source, model-agnostic AI coding agent that runs locally on your machine. It can edit files, run terminal commands, and work across multiple repos at once. Because it's model-agnostic, a lot of developers pair it with local LLMs like Ollama or LM Studio, so their code and data never leave their machine.&lt;/p&gt;

&lt;p&gt;But OpenCode can't search the web for live data unless you give it access. You can do that with the &lt;a href="https://github.com/serpapi/opencode-serpapi" rel="noopener noreferrer"&gt;SerpApi OpenCode plugin&lt;/a&gt;. It gives OpenCode access to Google, YouTube, Amazon, Walmart, and dozens of other search engines through a single tool call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an OpenCode Plugin?
&lt;/h2&gt;

&lt;p&gt;Plugins and Skills sound similar, but they do different jobs. Skills automate boring workflows, tasks where you'd otherwise repeat the same instructions every time. Plugins give OpenCode a new capability it didn't have before, a new tool, feature, or integration with an outside service.&lt;/p&gt;

&lt;p&gt;SerpApi has released one such plugin, giving OpenCode real-time web search. It provides access to multiple SerpApi engines like Google, Bing, Yahoo, YouTube, and many others. It also ships with skills that guide OpenCode in selecting appropriate engine and parameters for each query.&lt;/p&gt;

&lt;p&gt;So when a task needs live web data, OpenCode reaches for the SerpApi plugin, runs the search, and returns the results. Say OpenCode needs to find a current public GitHub repository for a coding task. It searches for it with the SerpApi plugin instead of guessing from training memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use SerpApi Opencode Web Search Plugin?
&lt;/h2&gt;

&lt;p&gt;Without the plugin, giving OpenCode web access means building a separate API integration yourself: handling auth, setting parameters, and parsing responses.&lt;/p&gt;

&lt;p&gt;The SerpApi OpenCode plugin replaces all of that with one integration. Install it once, and OpenCode gets dedicated access to specific engines and search verticals instead of just generic web search.&lt;/p&gt;

&lt;p&gt;Here are some of the popular engines the plugin supports:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vertical&lt;/th&gt;
&lt;th&gt;Supported engines&lt;/th&gt;
&lt;th&gt;Use cases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web Search&lt;/td&gt;
&lt;td&gt;Google, Bing, DuckDuckGo, Yahoo, Yandex, Baidu, Naver&lt;/td&gt;
&lt;td&gt;Full-featured web search with knowledge graph, ads, and SERP extras&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI Search&lt;/td&gt;
&lt;td&gt;Google AI Mode, Google AI Overview, Bing Copilot, Brave AI Mode&lt;/td&gt;
&lt;td&gt;AI search results, brand monitoring in AI results, and AI-generated summaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shopping&lt;/td&gt;
&lt;td&gt;Amazon, Walmart, eBay, Google Shopping, Home Depot&lt;/td&gt;
&lt;td&gt;Product search, listings, and pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Google Maps&lt;/td&gt;
&lt;td&gt;Local business listings and place reviews&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;News &amp;amp; Trends&lt;/td&gt;
&lt;td&gt;Google News, Google Trends&lt;/td&gt;
&lt;td&gt;Current news articles and search trend data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media&lt;/td&gt;
&lt;td&gt;Google Images, Google Videos, Google Lens, YouTube&lt;/td&gt;
&lt;td&gt;Image search, video search, reverse image search, and video rankings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Travel&lt;/td&gt;
&lt;td&gt;Google Flights, Google Hotels, Google Travel Explore&lt;/td&gt;
&lt;td&gt;Flight deals, hotel reviews, and travel destination search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jobs, Finance &amp;amp; Apps&lt;/td&gt;
&lt;td&gt;Google Jobs, Google Finance, Google Play, Apple App Store&lt;/td&gt;
&lt;td&gt;Job search, stock tracking, finance data analysis, and app store data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO&lt;/td&gt;
&lt;td&gt;Google Autocomplete&lt;/td&gt;
&lt;td&gt;Keyword research&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each engine returns data shaped for the question you're asking. For instance, searching Amazon returns product listings and pricing in clean JSON. You also don't need to remember engine names or query formats. OpenCode picks the right engine and parameters based on the query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the OpenCode SerpApi Plugin
&lt;/h2&gt;

&lt;p&gt;The OpenCode SerpApi plugin is available as an &lt;a href="https://www.npmjs.com/package/opencode-serpapi" rel="noopener noreferrer"&gt;npm package&lt;/a&gt;, so setup is really just a config change plus an API key.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Add the plugin to your config
&lt;/h3&gt;

&lt;p&gt;In your project root, open &lt;code&gt;opencode.json&lt;/code&gt; (or &lt;code&gt;opencode.jsonc&lt;/code&gt;). If neither file exists, create &lt;code&gt;opencode.json&lt;/code&gt;. Then add &lt;code&gt;opencode-serpapi&lt;/code&gt; to the plugin array:&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;"$schema"&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://opencode.ai/config.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugin"&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="s2"&gt;"opencode-serpapi"&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;Want it available across all your projects instead of just one? Add the same block to your global config at &lt;code&gt;~/.config/opencode/opencode.json&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;OpenCode installs it automatically from npm the next time you run opencode in that project; doesn’t require a separate &lt;code&gt;npm i&lt;/code&gt; command.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Set your SerpApi API key
&lt;/h3&gt;

&lt;p&gt;Grab a key from your &lt;a href="https://serpapi.com/manage-api-key" rel="noopener noreferrer"&gt;SerpApi dashboard&lt;/a&gt; and export it as an environment variable:&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;"add_your_api_key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Start OpenCode
&lt;/h3&gt;

&lt;p&gt;On startup, OpenCode caches the plugin and registers the &lt;code&gt;serpapi_search&lt;/code&gt; tool for your session.&lt;/p&gt;

&lt;p&gt;Ask OpenCode something that needs a live web search, e.g.:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Search Google for the latest news on the Fed rate decision"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It returns live search results, as shown in the image below:&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%2Fj9qloqwlskucazq5hqe8.jpg" 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%2Fj9qloqwlskucazq5hqe8.jpg" alt="OpenCode web search plugin" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;OpenCode web search plugin&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How OpenCode uses SerpApi under the hood?
&lt;/h2&gt;

&lt;p&gt;The plugin has three parts, each with a clear job: a connection to SerpApi's MCP server, a native search tool as a backup, and a skill that tells OpenCode which search engine to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The MCP connection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When OpenCode starts, the plugin automatically registers &lt;a href="https://serpapi.com/blog/introducing-serpapis-mcp-server/" rel="noopener noreferrer"&gt;SerpApi's hosted MCP server&lt;/a&gt; as a remote MCP server. You don't need to add it to your config by hand.&lt;/p&gt;

&lt;p&gt;The plugin reads your &lt;code&gt;SERPAPI_API_KEY&lt;/code&gt; environment variable and sends it as an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header with each request.&lt;/p&gt;

&lt;p&gt;The server then checks the engine and its parameters, and runs the search. When OpenCode doesn't need the full JSON, it sets the engine's &lt;code&gt;output&lt;/code&gt; parameter to Markdown, which &lt;a href="https://serpapi.com/markdown-output" rel="noopener noreferrer"&gt;uses about half the tokens for the same task&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The &lt;code&gt;serpapi_search&lt;/code&gt; fallback&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the MCP server isn't available, OpenCode can still search. The plugin exposes a native &lt;code&gt;serpapi_search&lt;/code&gt; tool that calls SerpApi's &lt;code&gt;search.json&lt;/code&gt; endpoint directly, using the runtime's built-in &lt;code&gt;fetch&lt;/code&gt;. The tool takes a single &lt;code&gt;q&lt;/code&gt; argument and maps it to the field each engine expects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The &lt;code&gt;serpapi-web-search&lt;/code&gt; skill&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The connection gives OpenCode access to various engines. The skill tells it which one fits the task, using bundled routing guidance and engine reference files.&lt;/p&gt;

&lt;p&gt;Ask OpenCode to find standing desks under $300 on Walmart, and the skill points it to the Walmart engine and its parameters. Without it, OpenCode might run a generic Google search and get blog posts and ads instead of product listings.&lt;/p&gt;

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

&lt;p&gt;This SerpApi plugin makes web search a native capability for OpenCode. It's open source, and setup just takes a config change and an API key.&lt;/p&gt;

&lt;p&gt;Check out the&lt;a href="https://github.com/serpapi/opencode-serpapi" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; if you want to see how it's built, report an issue, or request a feature.&lt;/p&gt;

&lt;p&gt;We are continuing to expand SerpApi integrations to make sure AI can access live search data, whether that is through &lt;a href="https://github.com/serpapi/serpapi-mcp" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;, &lt;a href="https://github.com/serpapi/serpapi-claude-plugin" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt;, and many other AI integrations. If you dont find a SerpApi integration for the tool you work in, contact us at &lt;a href="mailto:contact@serpapi.com"&gt;contact@serpapi.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>opensource</category>
      <category>tools</category>
    </item>
    <item>
      <title>Comparing Markdown Search Results: SerpApi vs. Exa, Tavily, and Firecrawl</title>
      <dc:creator>Tomás Murúa</dc:creator>
      <pubDate>Mon, 05 Oct 2026 01:52:21 +0000</pubDate>
      <link>https://dev.to/serpapi/comparing-markdown-search-results-serpapi-vs-exa-tavily-and-firecrawl-3opn</link>
      <guid>https://dev.to/serpapi/comparing-markdown-search-results-serpapi-vs-exa-tavily-and-firecrawl-3opn</guid>
      <description>&lt;p&gt;Tavily, Exa, Firecrawl, and SerpApi all return markdown for LLMs, but they aren't converting the same thing. Tavily, Exa, and Firecrawl convert the pages a search found. SerpApi converts the search itself, so you get the results page from the real search engine as clean markdown. &lt;a href="https://serpapi.com/markdown-output" rel="noopener noreferrer"&gt;Markdown Output&lt;/a&gt; works the same way across all of our 100+ search APIs.&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%2Foull8vi14d8latvc3fty.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%2Foull8vi14d8latvc3fty.png" alt="SerpApi markdown for coffee at 13,025 tokens next to Tavily, Exa, and Firecrawl page bodies at 90,234 to 133,343 tokens" width="800" height="572"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This blog runs the same three queries through all four providers, using SerpApi's &lt;a href="https://serpapi.com/search-api" rel="noopener noreferrer"&gt;Google Search API&lt;/a&gt; as the baseline. It compares where the markdown lives in each response, what it costs in tokens, and what a model can answer from it. By the end, you'll know which output fits your agent: the results page, a list of links, or the pages behind them.&lt;/p&gt;

&lt;p&gt;We've already compared positioning and pricing in &lt;a href="https://serpapi.com/blog/serpapi-vs-firecrawl/" rel="noopener noreferrer"&gt;SerpApi vs. Firecrawl&lt;/a&gt;, &lt;a href="https://serpapi.com/blog/serpapi-vs-exa-ai/" rel="noopener noreferrer"&gt;SerpApi vs. Exa&lt;/a&gt;, and &lt;a href="https://serpapi.com/blog/serpapi-alternatives-best-web-search-apis/" rel="noopener noreferrer"&gt;SerpApi Alternatives: Best Web Search APIs&lt;/a&gt;. This one is only about the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  How We Compared&amp;nbsp;
&lt;/h2&gt;

&lt;p&gt;Every provider got the same query on its playground defaults, on a free plan, with generated answers turned off. The only settings we changed are the content modes named in each table and the location. We set the location as close to Austin, Texas, as each provider allows, which meant the United States for Tavily and Exa, and Austin for Firecrawl. The SerpApi baseline is &lt;a href="https://serpapi.com/search-api" rel="noopener noreferrer"&gt;Google Search&lt;/a&gt; with &lt;code&gt;location=Austin, Texas, United States&lt;/code&gt;, &lt;code&gt;gl=us&lt;/code&gt;, and &lt;code&gt;hl=en&lt;/code&gt;, the same defaults as the &lt;a href="https://serpapi.com/playground" rel="noopener noreferrer"&gt;playground&lt;/a&gt;. We also set &lt;a href="https://serpapi.com/search-api#api-parameters-serpapi-parameters-no-cache" rel="noopener noreferrer"&gt;no_cache=true&lt;/a&gt; so every SerpApi search was a live fetch.&lt;/p&gt;

&lt;p&gt;We ran three queries, each one testing something different:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;What it tests&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;coffee&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;SERP features. A one-word query where Google shows most of them at once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;how to make delicious coffee&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Semantic question. The kind of question an agent forwards from a user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;grok 4.7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Freshness. Announced 45 minutes before capture, so it shows who searches live and who reads from an index&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Token counts use &lt;a href="https://github.com/openai/tiktoken" rel="noopener noreferrer"&gt;tiktoken&lt;/a&gt; with the &lt;code&gt;o200k_base&lt;/code&gt; encoding on the full saved response, JSON or markdown, nothing stripped. Each provider section shows a trimmed copy of its response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 1: SERP Features
&lt;/h2&gt;

&lt;p&gt;Here is every mode side by side for coffee.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;What the tokens buy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi&lt;/td&gt;
&lt;td&gt;Google Search, JSON&lt;/td&gt;
&lt;td&gt;66,237&lt;/td&gt;
&lt;td&gt;Full SERP as structured objects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SerpApi&lt;/td&gt;
&lt;td&gt;Google Search, markdown&lt;/td&gt;
&lt;td&gt;13,025&lt;/td&gt;
&lt;td&gt;Full SERP as a document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;basic&lt;/code&gt;, snippets only&lt;/td&gt;
&lt;td&gt;5,283&lt;/td&gt;
&lt;td&gt;10 titles, URLs, snippets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;basic&lt;/code&gt;, &lt;code&gt;include_raw_content=markdown&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;90,234&lt;/td&gt;
&lt;td&gt;8 titles plus 7 page bodies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tavily&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;advanced&lt;/code&gt;, &lt;code&gt;include_raw_content=markdown&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;104,792&lt;/td&gt;
&lt;td&gt;10 titles plus 10 page bodies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exa&lt;/td&gt;
&lt;td&gt;bare&lt;/td&gt;
&lt;td&gt;701&lt;/td&gt;
&lt;td&gt;10 titles and URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exa&lt;/td&gt;
&lt;td&gt;&lt;code&gt;highlights&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14,172&lt;/td&gt;
&lt;td&gt;10 titles plus extracted passages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exa&lt;/td&gt;
&lt;td&gt;&lt;code&gt;text&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;133,343&lt;/td&gt;
&lt;td&gt;10 titles plus 10 page bodies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecrawl&lt;/td&gt;
&lt;td&gt;search only&lt;/td&gt;
&lt;td&gt;8,747&lt;/td&gt;
&lt;td&gt;10 titles, URLs, descriptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firecrawl&lt;/td&gt;
&lt;td&gt;search + scrape, markdown&lt;/td&gt;
&lt;td&gt;130,041&lt;/td&gt;
&lt;td&gt;10 titles plus 9 page bodies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things to read from this table. The cheapest rows carry almost nothing, 10 links and at most a snippet each. Among the responses a model can actually answer from, SerpApi markdown is the smallest at 13,025 tokens. The sections below show one of these responses from each provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  SerpApi
&lt;/h3&gt;

&lt;p&gt;SerpApi scrapes the Google results page in real time and returns it as JSON or as markdown. The &lt;a href="https://serpapi.com/markdown-output" rel="noopener noreferrer"&gt;Markdown Output page&lt;/a&gt; covers how to use it, and our video on &lt;a href="https://www.youtube.com/watch?v=beqoAVXz7iA" rel="noopener noreferrer"&gt;supercharging AI agents with real-time data&lt;/a&gt; shows it inside an agent loop. Here we only look at the coffee response.&lt;/p&gt;

&lt;p&gt;The markdown is not a dump of the JSON. It is the results page curated for an LLM, with one section per SERP feature in the order Google showed them. The &lt;a href="https://serpapi.com/local-pack" rel="noopener noreferrer"&gt;local pack&lt;/a&gt; and &lt;a href="https://serpapi.com/organic-results" rel="noopener noreferrer"&gt;organic results&lt;/a&gt; become tables with links, and the &lt;a href="https://serpapi.com/knowledge-graph" rel="noopener noreferrer"&gt;knowledge graph&lt;/a&gt; becomes a list of its facts. Nothing inside the linked pages is fetched; what you get is what a person in Austin sees when they type coffee.&lt;/p&gt;

&lt;p&gt;This is the opening of the Google Search markdown document from the 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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;search_metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;6aa9bc62b9dbc418dd7b473f&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Success&lt;/span&gt;
  &lt;span class="na"&gt;json_endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://serpapi.com/searches/.../6aa9bc62b9dbc418dd7b473f.json"&lt;/span&gt;
  &lt;span class="na"&gt;markdown_endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://serpapi.com/searches/.../6aa9bc62b9dbc418dd7b473f.md"&lt;/span&gt;
  &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-09-15T21:45:06.935Z"&lt;/span&gt;
&lt;span class="na"&gt;search_parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;engine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google&lt;/span&gt;
  &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Coffee&lt;/span&gt;
  &lt;span class="na"&gt;location_requested&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Austin, Texas, United States&lt;/span&gt;
  &lt;span class="na"&gt;hl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
  &lt;span class="na"&gt;gl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gu"&gt;## Search Information&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Query Displayed: Coffee
&lt;span class="p"&gt;-&lt;/span&gt; Total Results: 214
&lt;span class="p"&gt;-&lt;/span&gt; Organic Results State: Results for exact spelling

&lt;span class="gu"&gt;## Local Map&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;![Image&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://serpapi.com/searches/6aa9bc62b9dbc418dd7b473f/images/M0vMWN9e2SQWmXNdUKaKlQ.png&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;](https://www.google.com/search?q=Coffee&amp;amp;...&amp;amp;udm=1)

&lt;span class="gu"&gt;## Local Results&lt;/span&gt;

&lt;span class="gu"&gt;### Places&lt;/span&gt;

| Position | Title | Type | Rating | Reviews | Price | Description | Thumbnail | Thumbnail Large | Links Phone | Links Website | Place Id | Gps Coordinates Latitude | Gps Coordinates Longitude | Address | Hours | Phone |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | Houndstooth Coffee | Coffee shop | 4.5 | 1210 | $1–10 | "Excellent coffee, offer singe source and blends, the options always rotate" | !&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Thumbnail&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://serpapi.com/searches/6aa9bc62b9dbc418dd7b473f/images/-WgD2tu7k8gkCk2xfEgTRbF9GSQB9Snw18rTTBABMPE7QjI8PtOIbqd0wV7X81n3.jpeg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; | https://lh3.googleusercontent.com/gps-cs-s/AHRPTWnWTvhbq7JcIcVUxEmUhGnD40dIdSQbygpfH9KvFfmvSN9Hix8E60n3riRiG648DFrJz7Z8raNZfxcVWY5UbcOHkCyE56MPZoVV2rslKvPAVcTTZkyfbbVHBGYpciBgoKjzCcrVFA=w1000-h1000-c-n | tel:(512) 394-6051 | http://www.houndstoothcoffee.com/?utm_source=google&amp;amp;utm_medium=organic&amp;amp;utm_campaign=googlebusiness | 11265938073076301333 | 30.266216 | -97.743066 | 401 Congress Ave. #100c, Austin, TX 78701 | Open · Closes 7 PM | (512) 394-6051 |

&lt;span class="c"&gt;&amp;lt;!-- 2 more places, then 8 more sections, 13,025 tokens in total --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;This is the results page:&lt;/strong&gt; SerpApi returns what Google ranked, grouped, and labeled. The local pack, the knowledge graph, and the AI Overview exist here and nowhere else in this comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fifth of the JSON:&lt;/strong&gt; Markdown drops the JSON brackets and keys and keeps the text a model reads, which is the reason the feature exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trim what you don't need:&lt;/strong&gt; The &lt;a href="https://serpapi.com/json-restrictor" rel="noopener noreferrer"&gt;JSON Restrictor&lt;/a&gt; keeps only the sections you ask for, and it also works with Markdown output. For example, &lt;code&gt;json_restrictor=organic_results&lt;/code&gt; keeps only the organic results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every search has an ID and a URL:&lt;/strong&gt; &lt;code&gt;search_metadata.id&lt;/code&gt; and &lt;code&gt;markdown_endpoint&lt;/code&gt; are in the frontmatter. Open the URL later and you get the same document back.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tavily
&lt;/h3&gt;

&lt;p&gt;Tavily is a search API built for agents. A basic search for coffee returns 10 results with a title, a URL, a snippet in &lt;code&gt;content&lt;/code&gt;, and a relevance score. There is no markdown yet. The markdown appears when you set &lt;code&gt;include_raw_content&lt;/code&gt; to &lt;code&gt;markdown&lt;/code&gt;. It lands inside each result as &lt;code&gt;raw_content&lt;/code&gt;, the converted body of that result’s page. The response stays JSON and the markdown is a string field inside it.&lt;/p&gt;

&lt;p&gt;This is the first result of the basic search with &lt;code&gt;include_raw_content=markdown&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;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"coffee"&lt;/span&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;"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://en.wikipedia.org/wiki/Coffee"&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;"Coffee"&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;"Coffee is a beverage brewed from roasted ground coffee beans. Darkly colored and bitter, coffee has a stimulating effect on humans due to its caffeine content, [...]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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;0.83742094&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"raw_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;"[Jump to content](#bodyContent)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;[![](/static/images/icons/enwiki-25.svg)  ![Wikipedia](/static/images/mobile/copyright/wikipedia-wordmark-en-25.svg) ![The Free Encyclopedia](/static/images/mobile/copyright/wikipedia-tagline-en-25.svg)](/wiki/Main_Page)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;[Search](/wiki/Special:Search &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Search Wikipedia [f]&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;## Contents&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;* [1 Etymology](#Etymology)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* [2 History](#History)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  + [2.1 Legendary accounts and myths](#Legendary_accounts_and_myths)"&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="err"&gt;raw_content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;continues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;242&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;608&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;characters&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"favicon"&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://en.wikipedia.org/static/apple-touch/wikipedia.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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;"fe77ba-00"&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="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;more&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;raw_content&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;present&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="mi"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&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;"response_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_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;"6e4056c2-7185-4d66-ba80-44fe823d7392"&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;raw_content is the page, not the SERP:&lt;/strong&gt; For coffee the first result is Wikipedia, and raw_content starts with Wikipedia’s navigation links and table of contents. This response has no local pack, knowledge graph, or People Also Ask.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pages are the cost:&lt;/strong&gt; The snippets-only response is 5,283 tokens. The extra tokens in the other Tavily rows are the page bodies it adds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Exa
&lt;/h3&gt;

&lt;p&gt;Exa searches its own neural index of the web. The bare search returns 10 results with &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;publishedDate&lt;/code&gt; on some, and no snippet at all. Page content comes through two opt-in fields. The &lt;code&gt;highlights&lt;/code&gt; field is an array of passages Exa extracted from each page. The &lt;code&gt;text&lt;/code&gt; field is the full page body, which Exa returns as markdown-style text by default.&lt;/p&gt;

&lt;p&gt;This is the first result of the text search:&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;"requestId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6e0760b35bd59e7fd1699bc92570c7ff"&lt;/span&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;"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;"https://en.wikipedia.org/wiki/Coffee"&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;"Coffee"&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://en.wikipedia.org/wiki/Coffee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Coffee&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Coffee&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;| Latte and black filtered coffee | |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| --- | --- |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| Type | Usually hot; can be iced |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| Origin | Yemen [1] [2] [3] |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| Introduced | 15th century |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| Color | Black, dark brown, light brown, beige |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| Flavor | Distinctive, somewhat bitter |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| Ingredients | Roasted coffee beans |&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;| Standard drinkware | Mug |&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Coffee is a beverage brewed from roasted ground coffee beans."&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="err"&gt;text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;continues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;126&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;characters&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"image"&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://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Coffee_beans_unroasted.jpg/250px-Coffee_beans_unroasted.jpg"&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="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;more&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;results&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;"searchTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;880.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"costDollars"&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;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.007&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"search"&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;"neural"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.007&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;text is the page, not the SERP:&lt;/strong&gt; The Wikipedia result keeps its infobox as a markdown table, but nothing from Google's results page is in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;text is the largest response in this comparison:&lt;/strong&gt; Exa inlines the full body of all 10 pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Firecrawl
&lt;/h3&gt;

&lt;p&gt;Firecrawl is a page scraper with a &lt;code&gt;/search&lt;/code&gt; endpoint on top. With scraping off, the search returns 10 web results with &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, and &lt;code&gt;description&lt;/code&gt;. That is 8,747 tokens and no markdown. With “scrape content from search results” on and the format set to &lt;code&gt;markdown&lt;/code&gt;, each result gains a markdown field holding the main content of the page. It also gets a metadata object with the page’s own title, description, and status code.&lt;/p&gt;

&lt;p&gt;This is the first result of search + scrape with markdown format:&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;"web"&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;"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://en.wikipedia.org/wiki/Coffee"&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;"Coffee - Wikipedia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"For other uses, see [Coffee (disambiguation)](https://en.wikipedia.org/wiki/Coffee_(disambiguation))."&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;"markdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[Jump to content](https://en.wikipedia.org/wiki/Coffee#bodyContent)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;[![Page semi-protected](https://thumb.wikimedia.org/wikipedia/en/thumb/1/1b/Semi-protection-shackle.svg/20px-Semi-protection-shackle.svg.png)](https://en.wikipedia.org/wiki/Wikipedia:Protection_policy#semi &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;This article is semi-protected.&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;From Wikipedia, the free encyclopedia&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Brewed beverage&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;This article is about the beverage. For other uses, see [Coffee (disambiguation)](https://en.wikipedia.org/wiki/Coffee_(disambiguation) &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Coffee (disambiguation)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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="err"&gt;markdown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;continues&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;249&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;433&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;characters&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&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;"Coffee - Wikipedia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sourceURL"&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://en.wikipedia.org/wiki/Coffee"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cachedAt"&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-09-15T15:48:22.840Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cacheState"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"creditsUsed"&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="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;more&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;including&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;og:*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tags&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;copied&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;from&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;page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;head&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;more&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;markdown&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;present&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="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&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;"creditsUsed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pages can come from a cache:&lt;/strong&gt; The Wikipedia page above has &lt;code&gt;cacheState: hit&lt;/code&gt;, and its &lt;code&gt;cachedAt&lt;/code&gt; shows Firecrawl stored it about seven hours before our request. For &lt;code&gt;coffee&lt;/code&gt;, 6 of the 9 pages with markdown came from that cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search plus scrape is the expensive path:&lt;/strong&gt; It costs about 10 times the tokens of the SerpApi markdown for the same query, because the response includes 9 full pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Test 2: Semantic Question
&lt;/h2&gt;

&lt;p&gt;Now let’s test a question a user would ask an agent, how to make delicious coffee. Google shows a different page for it. The local pack and knowledge graph are gone, and in their place are recipes and videos. The related questions and the AI Overview are still there. The other three providers still return the top pages.&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%2Fq3dw70tis5lxst3w4bv5.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%2Fq3dw70tis5lxst3w4bv5.png" alt="SerpApi markdown with Google's AI Overview for a how-to query, next to Tavily, Exa, and Firecrawl page bodies without it" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tavily, Exa, and Firecrawl return the same kind of response we saw for coffee, page bodies inside JSON, so here we only show SerpApi. The part to look at is the &lt;a href="https://serpapi.com/ai-overview" rel="noopener noreferrer"&gt;AI Overview&lt;/a&gt;, Google’s own answer to the question. It comes in the markdown with its sources:&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;## Ai Overview&lt;/span&gt;

&lt;span class="gu"&gt;### Text Blocks&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; To make tasty coffee at home, start with freshly ground whole beans, a precise coffee-to-water ratio, and filtered water heated to the optimal temperature.
&lt;span class="p"&gt;2.&lt;/span&gt; Essential Rules for Great Coffee

&lt;span class="c"&gt;&amp;lt;!-- 7 more text blocks --&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;### References&lt;/span&gt;

| Source | Index | Title | Snippet | Thumbnail |
| --- | --- | --- | --- | --- |
| thekitchn.com | 2 | &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;https://www.thekitchn.com/best-coffee-brewing-method-22977756&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://www.thekitchn.com/best-coffee-brewing-method-22977756&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; |  |  |
| coffeechronicler.com | 3 | &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;https://coffeechronicler.com/best-way-to-make-coffee-at-home/&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://coffeechronicler.com/best-way-to-make-coffee-at-home/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; |  |  |

&lt;span class="c"&gt;&amp;lt;!-- 8 references in total, then Organic Results, Related Searches, Pagination --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent can quote this answer or open one of its sources. The other providers can’t return it, because the AI Overview only exists on Google's results page, not on any of the pages they fetch.&lt;/p&gt;

&lt;p&gt;For a how-to question, SerpApi returns Google's answer and its sources in 11,266 tokens. The next smallest full-page response was Exa's, at 32,772 tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test 3: Freshness
&lt;/h2&gt;

&lt;p&gt;The grok 4.7 query is the freshness test. xAI announced it on X, and we ran the query 45 minutes later. By then Google had indexed the announcement, but most of the web had not. A provider searching live should return the release. A provider reading from an index returns whatever it crawled last.&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%2Fpsvjqfw3rnufj9lfzkov.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%2Fpsvjqfw3rnufj9lfzkov.png" alt="Results about the Grok 4.7 launch, with SerpApi at 8 of 11, Tavily 0 of 10, Exa 2 of 10, and Firecrawl 3 of 10" width="800" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SerpApi's response is about a third the size of the others, but the bigger difference is the content. SerpApi returned Google's results page with the xAI announcement first and the launch coverage in &lt;a href="https://serpapi.com/top-stories" rel="noopener noreferrer"&gt;Top Stories&lt;/a&gt;. All 10 of Tavily's results were written before the launch, so a model reading them would answer that Grok 4.7 is not out yet. Exa ranked the announcement and Vercel's changelog first, then filled the rest with pre-launch speculation. Firecrawl found three posts about the release, and only the two Cursor forum threads came back with content.&lt;/p&gt;

&lt;p&gt;For breaking news, SerpApi returned the most coverage of the release. Eight of the 11 results across its organic results and Top Stories were about the launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results Across All Three Queries
&lt;/h2&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%2Fzmyvacsfzxcgvkjc2dur.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%2Fzmyvacsfzxcgvkjc2dur.png" alt="Bar chart of tokens per response, where SerpApi markdown is the smallest on all three queries" width="799" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On every query, SerpApi's markdown was the smallest response a model could answer from. It used 10,209 to 13,025 tokens, while the full-page modes in the chart used 30,802 to 133,343. The gap is widest on coffee, where all three full-page responses include the Wikipedia article on coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Location and Traceability
&lt;/h2&gt;

&lt;p&gt;The tests above compared what each provider returned. Two differences don't show up in a single response, but they matter once an agent runs in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizing the Search
&lt;/h3&gt;

&lt;p&gt;Each SerpApi engine exposes the main parameters its search engine offers, and the markdown follows them. For Google Search that means you can pin a query to what a specific user would see.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;What it controls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;location&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;City, state, or neighborhood the search comes from&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Country of the results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Interface language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google_domain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which Google domain to query, such as &lt;code&gt;google.co.uk&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;device&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Desktop, tablet, or mobile layout&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The other three providers accept at best a country or a city. Each engine lists its parameters in its docs, for example the &lt;a href="https://serpapi.com/search-api" rel="noopener noreferrer"&gt;Google Search API docs&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trace Every Search
&lt;/h3&gt;

&lt;p&gt;Every SerpApi response, JSON or markdown, opens with &lt;code&gt;search_metadata&lt;/code&gt;. It has the search id, a &lt;code&gt;json_endpoint&lt;/code&gt;, a &lt;code&gt;markdown_endpoint&lt;/code&gt;, and the &lt;code&gt;created_at&lt;/code&gt; timestamp. The &lt;a href="https://serpapi.com/search-archive-api" rel="noopener noreferrer"&gt;Search Archive API&lt;/a&gt; keeps each search for 31 days, so the same &lt;code&gt;id&lt;/code&gt; returns the same results page later, in either format. Every SerpApi excerpt in this post came from an archived search.&lt;/p&gt;

&lt;p&gt;For an agent, a log line with a search id is a record of exactly what the model read. If a user asks why the model said something, you can open the search and check. Tavily returns a &lt;code&gt;request_id&lt;/code&gt; and Exa a &lt;code&gt;requestId&lt;/code&gt;, and neither can fetch that search again. Firecrawl returns a search id, plus a &lt;code&gt;scrapeId&lt;/code&gt; and &lt;code&gt;cacheState&lt;/code&gt; for each page, but its API documents no way to fetch the search again. With those three, the only record is the copy you kept.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Which
&lt;/h2&gt;

&lt;p&gt;The question is whether your agent needs the search or the pages behind 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%2Fvioymb832ukx1nn80iu9.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%2Fvioymb832ukx1nn80iu9.png" alt="Decision guide for when an agent needs the search from SerpApi or the pages from Tavily, Exa, or Firecrawl" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tavily, Exa, and Firecrawl aren't doing anything wrong. Returning pages is the product, and for a research agent that needs to read every source, it is the right product. The token counts above are the size of those pages, not a flaw.&lt;/p&gt;

&lt;p&gt;The table below shows each case in more detail, with the reason behind each pick:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need&lt;/th&gt;
&lt;th&gt;Pick&lt;/th&gt;
&lt;th&gt;Because&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What Google shows for a query, including local pack, knowledge graph, and AI Overview&lt;/td&gt;
&lt;td&gt;SerpApi markdown&lt;/td&gt;
&lt;td&gt;The only response here with those sections, and the smallest one a model can answer from&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Results for something that happened this hour&lt;/td&gt;
&lt;td&gt;SerpApi markdown&lt;/td&gt;
&lt;td&gt;A live Google search, with Google's relative dates on most rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Results as a user in a specific city, country, or device would see them&lt;/td&gt;
&lt;td&gt;SerpApi markdown&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;location&lt;/code&gt;, &lt;code&gt;gl&lt;/code&gt;, &lt;code&gt;hl&lt;/code&gt;, and &lt;code&gt;device&lt;/code&gt; pin the search down to the neighborhood&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A record of what the model read&lt;/td&gt;
&lt;td&gt;SerpApi markdown&lt;/td&gt;
&lt;td&gt;Every search keeps an &lt;code&gt;id&lt;/code&gt; and a URL for 31 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fields to parse in code, such as ratings, prices, or positions&lt;/td&gt;
&lt;td&gt;SerpApi JSON&lt;/td&gt;
&lt;td&gt;Typed fields, no markdown to parse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A short list of URLs to decide what to fetch next&lt;/td&gt;
&lt;td&gt;Tavily &lt;code&gt;basic&lt;/code&gt;, Exa bare, Firecrawl search only&lt;/td&gt;
&lt;td&gt;Under 9,000 tokens each. Nothing to answer from, but nothing wasted either&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The full body of the pages behind a query&lt;/td&gt;
&lt;td&gt;Tavily &lt;code&gt;include_raw_content&lt;/code&gt;, Exa &lt;code&gt;text&lt;/code&gt;, Firecrawl search + scrape&lt;/td&gt;
&lt;td&gt;They return the pages. Expect 30,000 to 135,000 tokens, and clean the navigation yourself.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Any agent that needs to know what the web says right now, where a user would see it, and exactly what the model read needs the search before it needs the pages. SerpApi's Markdown Output returns that search as one document, from Google Search or any of our other 100+ search APIs.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Do Amazon Keyword Research with Python</title>
      <dc:creator>Noraina Nordin</dc:creator>
      <pubDate>Sat, 03 Oct 2026 09:41:12 +0000</pubDate>
      <link>https://dev.to/serpapi/how-to-do-amazon-keyword-research-with-python-1bof</link>
      <guid>https://dev.to/serpapi/how-to-do-amazon-keyword-research-with-python-1bof</guid>
      <description>&lt;p&gt;Amazon keyword research starts with one question: what do shoppers actually type into the search bar? The most direct answer is Amazon's own autocomplete. When you type "coffee b", Amazon suggests "coffee beans", "coffee bean grinder", and "coffee bar accessories". Those suggestions are coming from real search behavior.&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%2Fj1yco0dl4k5iemgvgz9y.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%2Fj1yco0dl4k5iemgvgz9y.png" alt="Amazon Homepage" width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amazon Homepage&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most free Amazon keyword research tools are built on the same data, but they limit how many lookups you can run, and it's hard for you to integrate them into your own workflow. In this tutorial, you'll use the &lt;a href="https://serpapi.com/amazon-autocomplete-api" rel="noopener noreferrer"&gt;Amazon Autocomplete API&lt;/a&gt; from SerpApi to get these suggestions as a clean JSON or Markdown output, expand one seed keyword into dozens of long-tail terms, and save them to a CSV file.&lt;/p&gt;

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

&lt;p&gt;Each request returns a &lt;code&gt;suggestions&lt;/code&gt; array. Each suggestion includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;value&lt;/code&gt;:&lt;/strong&gt; the suggested search term, for example &lt;code&gt;coffee beans&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;type&lt;/code&gt;:&lt;/strong&gt; the suggestion type. &lt;code&gt;KEYWORD&lt;/code&gt; is a regular search suggestion. &lt;code&gt;WIDGET&lt;/code&gt; is a grouped block, such as a price-range shortcut.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;items&lt;/code&gt;:&lt;/strong&gt; for &lt;code&gt;WIDGET&lt;/code&gt; suggestions, a list of options, each with its own &lt;code&gt;value&lt;/code&gt; and &lt;code&gt;amazon_link&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;thumbnail&lt;/code&gt;:&lt;/strong&gt; a small product image for the suggestion, when Amazon shows one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;amazon_link&lt;/code&gt;:&lt;/strong&gt; the Amazon search results URL for that suggestion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;serpapi_link&lt;/code&gt;:&lt;/strong&gt; a ready-made SerpApi request that gets autocomplete suggestions for this suggestion. Useful for going one level deeper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;serpapi_amazon_link&lt;/code&gt;:&lt;/strong&gt; a ready-made &lt;a href="https://serpapi.com/amazon-search-api" rel="noopener noreferrer"&gt;Amazon Search API&lt;/a&gt; request for this suggestion, so you can see which products rank for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the result from the trimmed response for &lt;code&gt;coffee&lt;/code&gt; 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;"suggestions"&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Coffee table by price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WIDGET"&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="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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Under $100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"amazon_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/s?k=coffee+table&amp;amp;amp=&amp;amp;rh=n%3A1063318%2Cp_n_price%3A8570764011&amp;amp;amp=&amp;amp;rnid=8570763011&amp;amp;amp=&amp;amp;nav_sdd=aps&amp;amp;amp=&amp;amp;crid=26CJG6W215CUX&amp;amp;amp=&amp;amp;sprefix=coffee&amp;amp;amp=&amp;amp;ref=nb_sb_ss_w_sbl-tr-t1_k0_1_6_0"&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="err"&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;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"coffee table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"KEYWORD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"thumbnail"&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://m.media-amazon.com/images/I/41Q0IEeqGBL.__AC_SL75__.jpg"&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_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;engine=amazon_autocomplete&amp;amp;k=coffee+table"&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_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;device=desktop&amp;amp;engine=amazon&amp;amp;k=coffee+table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"amazon_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/s?k=coffee%20table"&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="err"&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="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;&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%2Ffj07x4ei86hqpllrd10n.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%2Ffj07x4ei86hqpllrd10n.png" alt="SerpApi returns Amazon's search suggestions in a structured output" width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SerpApi returns Amazon's search suggestions 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;. The free plan includes 250 searches per month, and you can try any query live in the &lt;a href="https://serpapi.com/playground?engine=amazon_autocomplete" rel="noopener noreferrer"&gt;interactive playground&lt;/a&gt; before writing code.&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%2Fdu18aoi60qozcf687tw6.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%2Fdu18aoi60qozcf687tw6.png" alt="SerpApi API key dashboard" width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;SerpApi API key dashboard&lt;/em&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're sharing or publishing your code. If it's ever leaked, you can generate a new one from the dashboard. The examples below read the key from an environment variable.&lt;/p&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;p&gt;For the Python examples, install the official client:&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;h3&gt;
  
  
  Review the Amazon Autocomplete API documentation
&lt;/h3&gt;

&lt;p&gt;The engine is &lt;code&gt;amazon_autocomplete&lt;/code&gt;, and the text you'd type into Amazon's search bar goes in the &lt;code&gt;k&lt;/code&gt; parameter. Other useful parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;amazon_department&lt;/code&gt;: limit suggestions to one department. Defaults to &lt;code&gt;aps&lt;/code&gt; (All Departments). See the full list of &lt;a href="https://serpapi.com/amazon-departments" rel="noopener noreferrer"&gt;Amazon departments&lt;/a&gt;.&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;/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-autocomplete-api" rel="noopener noreferrer"&gt;Amazon Autocomplete 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%2Fcivwpupy7f1dsa9p17oe.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%2Fcivwpupy7f1dsa9p17oe.png" alt="SerpApi's Amazon Autocomplete API documentation" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  How to scrape Amazon autocomplete suggestions
&lt;/h2&gt;

&lt;p&gt;The results are identical 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 suggestions for "coffee":&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_autocomplete&amp;amp;k=coffee&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 returns the data in a more token-efficient format built with tables and links, which is handy when feeding results 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_autocomplete&amp;amp;k=coffee&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 suggestions for "coffee" and prints each keyword with its type, using the &lt;a href="https://serpapi.com/integrations/python" rel="noopener noreferrer"&gt;official SerpApi Python library&lt;/a&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="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_autocomplete&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;k&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;coffee&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;suggestion&lt;/span&gt; &lt;span class="ow"&gt;in&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;suggestions&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;suggestion&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&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;suggestion&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="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;Coffee table by price | WIDGET
coffee table | KEYWORD
coffee pods | KEYWORD
coffee | KEYWORD
coffee maker | KEYWORD
coffee creamer | KEYWORD
coffee beans | KEYWORD
coffee grinder | KEYWORD
coffee filters | KEYWORD
coffee bean grinder | KEYWORD
coffee machine | KEYWORD
&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;:&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_autocomplete&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;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;coffee&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;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;suggestion&lt;/span&gt; &lt;span class="k"&gt;of&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;suggestions&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;suggestion&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="s2"&gt; | &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;suggestion&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="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 suggestions 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_autocomplete"&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;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"coffee"&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;
  
  
  Filter suggestions by Amazon department and marketplace
&lt;/h2&gt;

&lt;p&gt;The suggestions for a broad word mix with unrelated products. For example, "coffee" in All Departments, Amazon suggests "coffee table" and "coffee grinder" alongside "coffee pods".&lt;/p&gt;

&lt;p&gt;You can set &lt;code&gt;amazon_department&lt;/code&gt; to narrow the results. With &lt;code&gt;amazon_department=grocery&lt;/code&gt;, the same query returns only food and drink terms: "coffee pods", "coffee creamer", "coffee syrup", "coffee k cups", "coffee mate", and "coffee substitutes".&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_autocomplete&amp;amp;k=coffee&amp;amp;amazon_department=grocery&amp;amp;api_key=YOUR_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To research another marketplace, use &lt;code&gt;amazon_domain&lt;/code&gt; with any of the &lt;a href="https://serpapi.com/amazon-domains" rel="noopener noreferrer"&gt;supported Amazon domains&lt;/a&gt;. Suggestions reflect what shoppers search for on that site:&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_autocomplete&amp;amp;k=coffee&amp;amp;amazon_domain=amazon.co.uk&amp;amp;api_key=YOUR_API_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build a free Amazon keyword research tool with Python
&lt;/h2&gt;

&lt;p&gt;Each request returns about 10 suggestions. To build a real keyword list, use the same trick as most Amazon keyword suggestion tools by adding each letter of the alphabet after your seed keyword. For example, "coffee a", "coffee b", "coffee c", and so on; each returns a different set of long-tail suggestions.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Runs the seed keyword plus one query per letter (27 queries in total)&lt;/li&gt;
&lt;li&gt;Keeps only &lt;code&gt;KEYWORD&lt;/code&gt; suggestions and skips &lt;code&gt;WIDGET&lt;/code&gt; blocks&lt;/li&gt;
&lt;li&gt;Removes duplicates&lt;/li&gt;
&lt;li&gt;Saves the list to &lt;code&gt;amazon_keywords.csv&lt;/code&gt;, with the query that found each keyword
&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;csv&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;string&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;SEED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coffee&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;DEPARTMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grocery&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# use "aps" for All Departments
&lt;/span&gt;&lt;span class="n"&gt;MAX_QUERIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;         &lt;span class="c1"&gt;# 1 seed + 26 letters; each query uses one search
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_suggestions&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="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_autocomplete&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;k&lt;/span&gt;&lt;span class="sh"&gt;"&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="s"&gt;amazon_department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DEPARTMENT&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&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;s&lt;/span&gt; &lt;span class="ow"&gt;in&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;suggestions&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="n"&gt;s&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;KEYWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SEED&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&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;SEED&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;letter&lt;/span&gt;&lt;span class="si"&gt;}&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;letter&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ascii_lowercase&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;MAX_QUERIES&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;keywords&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;query&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queries&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;keyword&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;get_suggestions&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="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&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;query&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;amazon_keywords.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;keyword&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;source_query&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;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&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;Ran &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;queries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; queries and 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;keywords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; unique keywords to amazon_keywords.csv&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;blockquote&gt;
&lt;p&gt;💡 &lt;code&gt;MAX_QUERIES&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;&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%2Fxd0eb3meg0q7xhsk5hca.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%2Fxd0eb3meg0q7xhsk5hca.png" alt="Amazon keywords research output in CSV format" width="792" height="1036"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Amazon keywords research output in CSV format&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add modifiers.&lt;/strong&gt; Besides letters, try words shoppers use to narrow a search, such as "for", "best", "organic", or "with".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go one level deeper.&lt;/strong&gt; Run the script again with a top suggestion as the new seed, for example "coffee beans".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare marketplaces.&lt;/strong&gt; Add &lt;code&gt;amazon_domain&lt;/code&gt; and run the same seed on &lt;code&gt;amazon.com&lt;/code&gt; and &lt;code&gt;amazon.co.uk&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check which products rank for each keyword
&lt;/h2&gt;

&lt;p&gt;A keyword list tells you what shoppers search for. For Amazon product keyword research, the next step is to see what they find. Every &lt;code&gt;KEYWORD&lt;/code&gt; suggestion includes a &lt;code&gt;serpapi_amazon_link&lt;/code&gt;, which is a ready-made &lt;a href="https://serpapi.com/amazon-search-api" rel="noopener noreferrer"&gt;Amazon Search API&lt;/a&gt; request for that term. You can call it to get the ranking products, their prices, ratings, and review counts.&lt;/p&gt;

&lt;p&gt;For a full walkthrough of the Amazon Search API, pagination, and product details, see our guide on how to scrape Amazon product data below:&lt;/p&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;/ul&gt;

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

&lt;h3&gt;
  
  
  Does Amazon have a keyword tool?
&lt;/h3&gt;

&lt;p&gt;Amazon doesn't offer a public keyword research tool for shoppers. Sellers can see some search term data in Seller Central, but the most accessible source of real search terms is the autocomplete dropdown in Amazon's search bar, which is what this API returns.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are keywords in Amazon?
&lt;/h3&gt;

&lt;p&gt;Amazon keywords are the words and phrases shoppers type into Amazon's search bar. Sellers use them in product titles, bullet points, backend search terms, and ad campaigns so their products show up for those searches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a free Amazon keyword research tool?
&lt;/h3&gt;

&lt;p&gt;Several free tools exist, but most limit how many lookups you can run. The script in this post is a free alternative for up to 250 searches per month on SerpApi's free plan. Each query uses one search.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Amazon Autocomplete API show search volume?
&lt;/h3&gt;

&lt;p&gt;No. It returns the suggestions Amazon shows, in the order Amazon shows them, but not search volume. Use the suggestions to find which terms exist, then check volume with a separate keyword tool if you need it.&lt;/p&gt;

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

&lt;p&gt;The &lt;a href="https://serpapi.com/amazon-autocomplete-api" rel="noopener noreferrer"&gt;Amazon Autocomplete API&lt;/a&gt; turns Amazon's search suggestions into structured JSON and Markdown output, so you can run Amazon keyword research in bulk instead of typing into the search bar one query at a time. You can then pair it with the &lt;a href="https://serpapi.com/blog/scrape-amazon-product-data-tutorial/" rel="noopener noreferrer"&gt;Amazon Search API&lt;/a&gt; to see which products rank for each keyword, or the &lt;a href="https://serpapi.com/amazon-product-api" rel="noopener noreferrer"&gt;Amazon Product API&lt;/a&gt; to &lt;a href="https://serpapi.com/blog/automate-amazon-price-tracking-to-see-real-world-inflation/" rel="noopener noreferrer"&gt;track prices on the products you find&lt;/a&gt;.&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>
      <category>python</category>
      <category>seo</category>
      <category>tutorial</category>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
