<?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: Emmanuel Uchenna</title>
    <description>The latest articles on DEV Community by Emmanuel Uchenna (@eunit).</description>
    <link>https://dev.to/eunit</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%2Fuser%2Fprofile_image%2F118821%2F5bddf58c-edd7-47c2-a806-44f549477601.jpg</url>
      <title>DEV Community: Emmanuel Uchenna</title>
      <link>https://dev.to/eunit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eunit"/>
    <language>en</language>
    <item>
      <title>How to optimize CMS images in batches with Apify</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Sat, 31 Oct 2026 15:30:51 +0000</pubDate>
      <link>https://dev.to/eunit/how-to-optimize-cms-images-in-batches-with-apify-2egh</link>
      <guid>https://dev.to/eunit/how-to-optimize-cms-images-in-batches-with-apify-2egh</guid>
      <description>&lt;p&gt;An image optimization task sounds simple until you have a few hundred files, several output formats, and a content team waiting to publish. A smaller file helps only if it still looks right, keeps the dimensions your page needs, and has a reliable destination in your media library. When managing growing content libraries or building an &lt;a href="https://eunit.me/blog/automated-seo-pipeline" rel="noopener noreferrer"&gt;automated SEO pipeline&lt;/a&gt;, unoptimized media assets quickly become a primary bottleneck for web performance.&lt;/p&gt;

&lt;p&gt;This article walks you through using an &lt;a href="https://docs.apify.com/actors?fpr=eunit" rel="noopener noreferrer"&gt;Apify Actor&lt;/a&gt;, &lt;a href="https://apify.com/eunit/sqoosh-image-compressor" rel="noopener noreferrer"&gt;Sqoosh Image Compressor&lt;/a&gt;, to turn a list of source images into lightweight, modern web formats in batches. The Actor was originally built upon the &lt;a href="https://github.com/GoogleChromeLabs/squoosh" rel="noopener noreferrer"&gt;open-source Squoosh project by Google Chrome Labs&lt;/a&gt; - hence its name on the Apify Store. The current release uses &lt;a href="https://sharp.pixelplumbing.com/" rel="noopener noreferrer"&gt;sharp&lt;/a&gt; and &lt;a href="https://www.libvips.org/" rel="noopener noreferrer"&gt;libvips&lt;/a&gt; for encoding under the hood. While the Actor preserves its existing Store name and common compatibility input keys, it no longer depends on legacy client-side Squoosh workers.&lt;/p&gt;

&lt;p&gt;In this guide, you will prepare a small sample list, request JPEG and WebP outputs, inspect the generated records and artifacts, and create a review mapping ready for a safe CMS import. The workflow stops at a reviewable output stage; replacing production CMS assets remains a deliberate, separate step requiring your CMS credentials and an approved mapping.&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%2Fj5i5z1eeofvxb3lxo3gv.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%2Fj5i5z1eeofvxb3lxo3gv.png" alt="How to optimize CMS images in batches with Apify" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the workflow produces
&lt;/h2&gt;

&lt;p&gt;Each successful source-image and output-codec pair produces two distinct outputs: a structured record in the run's default dataset and a downloadable binary file in the run's key-value store. For example, requesting both JPEG and WebP for a single image produces two dataset rows and two stored files. If a conversion fails, the Actor logs the error and excludes it from the successful outputs.&lt;/p&gt;

&lt;p&gt;The primary dataset fields describe the source URL, selected codec, output key, original byte size, and encoded byte size. Always inspect the actual dataset before feeding output keys into downstream automation.&lt;/p&gt;

&lt;p&gt;The compatibility key &lt;code&gt;mozjpeg&lt;/code&gt; encodes standard JPEG files through sharp; it does not invoke the legacy MozJPEG WebAssembly binary. Currently supported encoder keys are &lt;code&gt;mozjpeg&lt;/code&gt;, &lt;code&gt;webp&lt;/code&gt;, &lt;code&gt;avif&lt;/code&gt;, and &lt;code&gt;oxipng&lt;/code&gt; (which produces optimized PNGs). This release does not support formats such as JXL and WP2, and it fails explicitly rather than silently falling back to another format.&lt;/p&gt;

&lt;p&gt;Output files are stored as run artifacts. Download them or transfer them to permanent cloud storage before your Apify storage retention period expires. An Apify artifact URL is designed for inspection and data transfer, so avoid using it as a permanent CMS media link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Choose a representative image sample
&lt;/h2&gt;

&lt;p&gt;Start with images that you own or have permission to process. A useful first sample includes a photograph, a transparent graphic, an image with small text, and an unusually wide or tall banner. If you manage an e-commerce product catalog, include both a high-resolution product detail image and a thumbnail. Testing distinct asset types helps you quickly expose format-specific artifacts and quality issues.&lt;/p&gt;

&lt;p&gt;Make sure to provide direct, publicly accessible image URLs. A web page containing an embedded image is not the same as the raw image file. Similar to collecting source links in &lt;a href="https://eunit.me/blog/how-to-download-instagram-videos-and-reels-at-scale-with-apify" rel="noopener noreferrer"&gt;batch media workflows&lt;/a&gt;, URLs that require an active browser login session, an expired signed token, or access to a private network will fail to download.&lt;/p&gt;

&lt;p&gt;Maintain a simple source inventory with an internal asset ID, original URL, intended page role, current pixel dimensions, and whether transparency must be preserved. Add a dedicated column for the CMS asset ID if you plan to import results back into your database. Never rely on filenames alone to match assets, as different folders across large sites frequently share identical filenames.&lt;/p&gt;

&lt;p&gt;For your initial run, stick to two or three images. A small, bounded test proves your input and transfer path without burning compute or leaving you with hundreds of unreviewed conversions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Configure the image conversion
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://apify.com/eunit/sqoosh-image-compressor" rel="noopener noreferrer"&gt;Sqoosh Image Compressor&lt;/a&gt; in the Apify Console and configure a bounded JSON input. Click on the "&lt;strong&gt;Try for free&lt;/strong&gt;" button&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%2Fm0ollx6la0u6o384eiyc.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%2Fm0ollx6la0u6o384eiyc.png" alt="Sqoosh Image Compressor" width="800" height="788"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a minimal configuration requesting JPEG and WebP outputs for a single public test image:&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"images"&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;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://httpbin.org/image/png"&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;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"encode"&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;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mozjpeg"&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;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;75&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;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"webp"&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;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"quality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;75&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;&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%2F61alkg5wfu2htw4k9m2x.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%2F61alkg5wfu2htw4k9m2x.png" alt="Sqoosh Image Compressor - Inpu ton Apify" width="799" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Replace the test URL with an image from your own inventory for your acceptance check. Public test endpoints can change or go offline, so do not rely on them for production work.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;quality&lt;/code&gt; setting of &lt;code&gt;75&lt;/code&gt; serves as a practical baseline for evaluation, not a universal rule. Quality values are not directly comparable across different codecs. A photograph often looks clean at 75, whereas a software dashboard screenshot or a graphic with sharp typography may show visible compression artifacts at that same setting.&lt;/p&gt;

&lt;p&gt;The Actor enforces runtime safety bounds on image count, total downloaded bytes, pixel dimensions, output file sizes, and conversion duration. These boundaries prevent oversized images or runaway batches from exhausting resources. Review the input schema and documentation for exact limits before scaling up, and avoid bypassing limits without checking your source image dimensions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Run a small batch and inspect the evidence
&lt;/h2&gt;

&lt;p&gt;Run the Actor and wait for it to finish. Once complete, inspect both the dataset and the key-value store. A green "Succeeded" status indicates that the container completed its run, but you still need to verify the generated outputs. Confirm that the dataset contains the expected number of successful rows and that the corresponding files exist in storage.&lt;/p&gt;

&lt;p&gt;For the single-image input above requesting two codecs, you should see two successful rows: one JPEG and one WebP. If only one row appears, check the run log to see what failed rather than assuming both were processed.&lt;/p&gt;

&lt;p&gt;Download and inspect each generated image in an image viewer. Check the entire frame alongside fine details: sharp borders, fine text, gradients, skin tones, and transparent backgrounds. Verify the dimensions as well. An image that opens successfully but loses transparency, rotates unexpectedly, or renders at incorrect dimensions is not ready for production CMS use.&lt;/p&gt;

&lt;p&gt;The Actor's build has been validated with local and cloud JPEG and WebP outputs, and automated tests verify decoding for generated JPEG, WebP, AVIF, and PNG files. While this confirms that the encoding pipeline functions as designed, it does not guarantee that every output file will be smaller than its source or meet your team's visual standards.&lt;/p&gt;

&lt;p&gt;Below are the actual 100 × 100 JPEG and WebP outputs generated from the test canary run and stored in this repository's assets:&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%2Futja8e3bfot9vd8eq7qr.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%2Futja8e3bfot9vd8eq7qr.jpg" alt="Actual JPEG output from the private image conversion canary" width="100" height="100"&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz48lsn7u6qtniibwtnwr.webp" 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%2Fz48lsn7u6qtniibwtnwr.webp" alt="Actual WebP output from the private image conversion canary" width="100" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this test, the source PNG was converted to a 3,025-byte JPEG and a 2,558-byte WebP file. The repository's provenance record tracks the run ID, build ID, source URL, and SHA-256 hashes for full verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Measure file-size changes correctly
&lt;/h2&gt;

&lt;p&gt;Always calculate byte savings using the exact source asset as the baseline:&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;byte_savings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size_before&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="n"&gt;size_after&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size_before&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;size_after&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use positive measured file sizes.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes_saved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;size_before&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;size_after&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;percent_saved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&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;size_after&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;size_before&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&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;A negative savings value is a completely normal outcome: it simply means the encoded output is larger than the original file. Do not drop these rows or replace negative numbers with zero to make aggregate metrics look better. Tiny icons, already compressed assets, and lossless re-encodings often result in larger files.&lt;/p&gt;

&lt;p&gt;Keep visual quality checks separate from raw byte savings. An image that achieves a 70% size reduction but turns product packaging text blurry is unacceptable for production. A milder compression setting or an alternate format is often the better compromise. Also, if your CMS or CDN already performs on-the-fly transformations, verify whether pre-compressing assets delivers meaningful savings or merely introduces an extra layer of lossy compression.&lt;/p&gt;

&lt;p&gt;When presenting results to your team, report sample sizes, asset types, output formats, dimensions, total bytes saved, failed inputs, and rejected visual candidates. Avoid extrapolating one dramatic saving across an entire media library. High-performing assets complement broader site optimization, such as maintaining an &lt;a href="https://eunit.me/blog/how-to-generate-and-submit-an-xml-sitemap-the-ultimate-guide" rel="noopener noreferrer"&gt;XML sitemap and efficient crawl structure&lt;/a&gt;, but raw image compression alone does not guarantee higher search rankings without measuring real-world site delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Export outputs with the Apify API
&lt;/h2&gt;

&lt;p&gt;You can execute the same bounded run programmatically using Python (version 3.11 or later) and the official Apify 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; &lt;span class="s2"&gt;"apify-client&amp;gt;=3.3,&amp;lt;4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store your API token securely in an environment variable rather than hardcoding it in scripts, public notebooks, or client-side bundles.&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;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&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;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Launch a bounded run of the Sqoosh Image Compressor Actor
&lt;/span&gt;&lt;span class="n"&gt;run&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;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eunit/sqoosh-image-compressor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&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;https://httpbin.org/image/png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encode&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="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mozjpeg&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;quality&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webp&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;quality&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;75&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="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;run_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="n"&gt;max_total_charge_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&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;run&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="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Actor run was not found.&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="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&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;SUCCEEDED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Inspect the run before importing outputs: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Iterate through dataset records containing output metadata
&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script initiates the conversion and outputs the dataset records for verification. Orchestrating Apify Actors with Python mirrors patterns used in &lt;a href="https://eunit.me/blog/how-to-monitor-greenhouse-and-lever-job-changes-with-apify" rel="noopener noreferrer"&gt;monitoring scheduled tasks and scraping workflows&lt;/a&gt;. Use the returned output keys and the run's key-value store to download approved assets, keeping your internal asset ID mapped to each record for traceability.&lt;/p&gt;

&lt;p&gt;Note that the &lt;code&gt;max_total_charge_usd&lt;/code&gt; parameter sets a spending cap per run. Total costs for ongoing batch jobs depend on run frequency, total converted outputs, and active Actor pricing. Check the &lt;a href="https://apify.com/eunit/sqoosh-image-compressor/pricing" rel="noopener noreferrer"&gt;current Store pricing model&lt;/a&gt; before processing large batches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Prepare a CMS import mapping
&lt;/h2&gt;

&lt;p&gt;Create a structured reconciliation table containing one row for every source image and its replacement candidates. Include the CMS asset ID, source URL, chosen format, local output file path, dimensions, byte sizes, and an explicit approval status. If you generated multiple formats (such as WebP alongside JPEG fallback), map each format to its appropriate destination field instead of overwriting a single asset record.&lt;/p&gt;

&lt;p&gt;Different CMS architectures handle asset updates differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some CMS platforms require uploading a new media file to obtain a new asset ID, followed by updating references in existing articles or product records.&lt;/li&gt;
&lt;li&gt;Other platforms allow in-place asset replacements under the same ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep these operations distinct. Uploading an optimized file does not automatically update pages referencing the old URL. If your workflow involves publishing across content hubs, maintaining structured manifests - similar to synchronizing data in &lt;a href="https://eunit.me/blog/build-automated-seo-pipeline-sitemap-generator-and-google-indexer-to-notion" rel="noopener noreferrer"&gt;automated CMS and Notion pipelines&lt;/a&gt; - prevents broken image links.&lt;/p&gt;

&lt;p&gt;Test the update in a staging environment or on unpublished drafts first. Inspect pages across mobile and desktop breakpoints to verify that responsive &lt;code&gt;srcset&lt;/code&gt; definitions, aspect ratios, focal points, and alternative text remain intact. Image encoders adjust file bytes, but presentation settings remain the responsibility of your CMS configuration.&lt;/p&gt;

&lt;p&gt;Always retain original image files and maintain an explicit rollback mapping linking old asset IDs to new ones. If an automated import is interrupted, this mapping allows you to pinpoint where the process stopped and reverse changes if needed. A failed or missing conversion should never delete an original asset.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;A reliable image optimization pipeline produces verifiable files, maintains a clear link between sources and outputs, and provides a safe rollback mechanism before any CMS database is updated. By starting with a representative sample, verifying visual fidelity alongside byte counts, and scripting batch runs with Apify, you can automate image compression without risking broken layouts or degraded visuals.&lt;/p&gt;

&lt;p&gt;To explore the tool, visit &lt;a href="https://apify.com/eunit/sqoosh-image-compressor" rel="noopener noreferrer"&gt;Sqoosh Image Compressor on Apify&lt;/a&gt;. For additional technical references, check the &lt;a href="https://sharp.pixelplumbing.com/api-output/" rel="noopener noreferrer"&gt;sharp documentation&lt;/a&gt;, &lt;a href="https://docs.apify.com/platform/storage/key-value-store" rel="noopener noreferrer"&gt;Apify key-value store guides&lt;/a&gt;, and the Actor's input schema.&lt;/p&gt;

</description>
      <category>apify</category>
      <category>imagecompression</category>
      <category>webperf</category>
      <category>imageoptimization</category>
    </item>
    <item>
      <title>How to monitor Greenhouse and Lever job changes with Apify</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Sun, 25 Oct 2026 14:26:18 +0000</pubDate>
      <link>https://dev.to/eunit/how-to-monitor-greenhouse-and-lever-job-changes-with-apify-5ca3</link>
      <guid>https://dev.to/eunit/how-to-monitor-greenhouse-and-lever-job-changes-with-apify-5ca3</guid>
      <description>&lt;p&gt;If you have ever tried to track company job boards using a spreadsheet or a basic &lt;a href="https://www.eunit.me/blog/tags/Web%20Scraping" rel="noopener noreferrer"&gt;web scraper&lt;/a&gt;, you know how quickly the data falls apart (as detailed in our breakdown of &lt;a href="https://www.eunit.me/blog/headless-browsers-vs-serp-apis-why-your-playwright-scraper-fails-in-production" rel="noopener noreferrer"&gt;why Playwright and headless browser scrapers fail in production&lt;/a&gt;). Every scheduled refresh spits out hundreds of duplicate rows. A hiring manager fixes a typo in a job title, and your script alerts you to a "brand new role." Worse, a transient network error returns an empty page, and your system flags every active vacancy at the company as filled.&lt;/p&gt;

&lt;p&gt;A dependable &lt;strong&gt;Greenhouse job change monitor&lt;/strong&gt; requires a strict operational contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Track each opening by its persistent source ID rather than guessing by title.&lt;/li&gt;
&lt;li&gt;Compare observations only when an employer's board fetch is 100% complete.&lt;/li&gt;
&lt;li&gt;Freeze baseline state automatically if an API request fails, preventing false mass removals.&lt;/li&gt;
&lt;li&gt;Categorize changes into distinct, actionable events: &lt;code&gt;new&lt;/code&gt;, &lt;code&gt;updated&lt;/code&gt;, &lt;code&gt;removed-from-board&lt;/code&gt;, and &lt;code&gt;no-longer-matches&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, you will set up a reliable &lt;strong&gt;Apify hiring monitor&lt;/strong&gt; workflow using public Greenhouse and Lever endpoints and the &lt;strong&gt;&lt;a href="https://apify.com/eunit/ats-job-change-monitor" rel="noopener noreferrer"&gt;ATS Job Change Monitor&lt;/a&gt;&lt;/strong&gt; Actor on the &lt;a href="https://apify.com?fpr=eunit" rel="noopener noreferrer"&gt;Apify platform&lt;/a&gt;. An Applicant Tracking System (ATS) manages hiring pipelines for thousands of tech employers. While broader scrapers (such as our guide on &lt;a href="https://www.eunit.me/blog/how-to-scrape-linkedin-job-postings-with-python-a-step-by-step-guide" rel="noopener noreferrer"&gt;how to scrape LinkedIn job postings with Python&lt;/a&gt;) are effective for multi-platform &lt;a href="https://www.eunit.me/blog/tags/Job%20Postings%20Scraping" rel="noopener noreferrer"&gt;job postings scraping&lt;/a&gt;, querying primary ATS endpoints directly eliminates aggregator lag, anti-bot walls, and duplicate records.&lt;/p&gt;

&lt;p&gt;We verified this workflow against live employer boards, including Greenhouse setups like Stripe and Lever's public demo board, validating complete board pagination, state preservation during simulated failures, and zero-noise runs on unchanged boards.&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%2Frf0gtki5smiddqu1me4t.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%2Frf0gtki5smiddqu1me4t.png" alt="How to monitor Greenhouse and Lever job changes with Apify" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What this monitor observes
&lt;/h2&gt;

&lt;p&gt;The Actor interacts directly with the public, unauthenticated APIs that ATS providers use to power company career portals. Greenhouse documents these public endpoints in its &lt;a href="https://developers.greenhouse.io/job-board.html" rel="noopener noreferrer"&gt;Job Board API&lt;/a&gt;, while Lever details its public endpoints in the &lt;a href="https://github.com/lever/postings-api" rel="noopener noreferrer"&gt;Lever postings API repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To monitor a company, you only need its public board token (slug):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Greenhouse token&lt;/strong&gt;: The identifier in &lt;code&gt;boards.greenhouse.io/{boardToken}&lt;/code&gt; (for example, &lt;code&gt;stripe&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lever token&lt;/strong&gt;: The site slug in &lt;code&gt;jobs.lever.co/{boardToken}&lt;/code&gt; (for example, &lt;code&gt;leverdemo&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need an applicant account, recruiter credentials, or an internal ATS API key. The Actor observes public job postings exactly as prospective applicants see them.&lt;/p&gt;

&lt;p&gt;Each record extracted from these endpoints is normalized into a predictable schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core identifiers&lt;/strong&gt;: &lt;code&gt;id&lt;/code&gt; (a composite unique key), &lt;code&gt;source_job_id&lt;/code&gt;, &lt;code&gt;provider&lt;/code&gt;, and &lt;code&gt;board_token&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job metadata&lt;/strong&gt;: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;job_url&lt;/code&gt;, &lt;code&gt;apply_url&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, &lt;code&gt;department&lt;/code&gt;, and &lt;code&gt;employment_type&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change tracking&lt;/strong&gt;: &lt;code&gt;change_type&lt;/code&gt;, &lt;code&gt;changed_fields&lt;/code&gt;, &lt;code&gt;observed_at&lt;/code&gt;, and source timestamps (&lt;code&gt;source_created_at&lt;/code&gt;, &lt;code&gt;source_updated_at&lt;/code&gt;) when exposed by the ATS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional content&lt;/strong&gt;: Job description text (with configurable character limits).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Missing fields remain &lt;code&gt;null&lt;/code&gt; rather than placeholder defaults. This tool observes public listings; it does not collect applicant records, reveal hiring managers, guess salary ranges, or determine whether an employer sponsors visas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a snapshot and a change feed are different
&lt;/h2&gt;

&lt;p&gt;A snapshot and a change feed answer fundamentally different operational questions. When deciding how to &lt;strong&gt;monitor job board changes&lt;/strong&gt;, choosing the right mode determines whether you get an audit of current openings or an event stream of what just happened for &lt;a href="https://www.eunit.me/blog/tags/Competitive%20Intelligence" rel="noopener noreferrer"&gt;competitive intelligence&lt;/a&gt; and &lt;a href="https://www.eunit.me/blog/tags/Market%20Research" rel="noopener noreferrer"&gt;market research&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output mode / event&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;th&gt;Ideal use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snapshot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;What matching jobs are currently published on this board right now?&lt;/td&gt;
&lt;td&gt;Initial data ingestion, directory seeding, and point-in-time hiring audits.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Which matching job IDs appeared that were not in your saved baseline?&lt;/td&gt;
&lt;td&gt;Instant alerts for newly published openings, &lt;a href="https://www.eunit.me/blog/tags/Lead%20Generation" rel="noopener noreferrer"&gt;lead generation&lt;/a&gt;, and competitor expansion signals.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Updated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Which specific fields (such as title, location, or department) changed on an existing job?&lt;/td&gt;
&lt;td&gt;Tracking job title revisions, compensation updates, or location policy shifts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Removed from board&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Which previously tracked job IDs vanished from a confirmed complete board fetch?&lt;/td&gt;
&lt;td&gt;Detecting closed positions, filled roles, or paused requisitions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;No longer matches&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Which tracked jobs remain active on the board but no longer meet your filters?&lt;/td&gt;
&lt;td&gt;Keeping saved baselines clean when refining keywords or team criteria.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Precision in language is critical. An event labeled &lt;code&gt;removed-from-board&lt;/code&gt; does not definitively mean a role was filled. An employer might temporarily unpublish a post, pull a requisition, migrate to another ATS, or consolidate listings. Similarly, &lt;code&gt;new&lt;/code&gt; means new to your monitored workflow baseline, not necessarily drafted five minutes ago.&lt;/p&gt;

&lt;p&gt;Always keep your collection timestamp (&lt;code&gt;observed_at&lt;/code&gt;) separate from the employer's publication timestamps (&lt;code&gt;source_created_at&lt;/code&gt; or &lt;code&gt;source_updated_at&lt;/code&gt;). If an ATS does not expose when a listing was updated, leave the field &lt;code&gt;null&lt;/code&gt;. Never substitute your scraping timestamp for the employer's actual publication date.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow architecture
&lt;/h2&gt;

&lt;p&gt;To keep your pipeline dependable and easy to debug, separate the source collection, state comparison, and downstream notification steps:&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%2Fy2ozzzs8jbmir4lwd72b.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%2Fy2ozzzs8jbmir4lwd72b.png" alt="The workflow architecture" width="798" height="113"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This decoupled architecture enforces two crucial reliability principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fail-safe state preservation&lt;/strong&gt;: If an employer's board fetch fails, times out, or returns truncated data, comparison stops immediately for that board. The Actor preserves the previous baseline intact. When the employer board recovers on a subsequent run, your feed resumes cleanly without triggering phantom mass deletions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delivery decoupling&lt;/strong&gt;: Writing detected changes to the Apify dataset is separate from pushing data to your external CRM, database, or Slack channel. If an external webhook fails, your dataset records remain safely preserved with their run IDs, allowing you to retry delivery without re-scraping the ATS.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: Use the ATS Job Change Monitor Actor
&lt;/h2&gt;

&lt;p&gt;First of all, we will use the &lt;strong&gt;&lt;a href="https://apify.com/eunit/ats-job-change-monitor" rel="noopener noreferrer"&gt;ATS Job Change Monitor&lt;/a&gt;&lt;/strong&gt; Actor on the &lt;a href="https://apify.com?fpr=eunit" rel="noopener noreferrer"&gt;Apify platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then click on "&lt;strong&gt;Try for free&lt;/strong&gt;" to get started.&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%2Fgwcon6f2u5ueqwhf4a6r.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%2Fgwcon6f2u5ueqwhf4a6r.png" alt="ATS Job Change Monitor Actor" width="800" height="788"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Choose a small employer watchlist
&lt;/h2&gt;

&lt;p&gt;Start with a focused watchlist of companies you actively want to monitor. A balanced initial test pairs one Greenhouse board with one Lever board:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the employer's public careers page in your browser.&lt;/li&gt;
&lt;li&gt;Locate the board slug in the URL:
&amp;nbsp; &amp;nbsp;- For Greenhouse: &lt;code&gt;https://boards.greenhouse.io/stripe&lt;/code&gt; yields the token &lt;code&gt;stripe&lt;/code&gt;.
&amp;nbsp; &amp;nbsp;- For Lever: &lt;code&gt;https://jobs.lever.co/leverdemo&lt;/code&gt; yields the token &lt;code&gt;leverdemo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verify that the token loads valid public jobs before configuring automated monitoring.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The optional &lt;code&gt;companyLabel&lt;/code&gt; parameter is your custom display name for reporting. It is not an identity validated by the ATS.&lt;/p&gt;

&lt;p&gt;The Actor supports between 1 and 10 employer boards per run. Begin with two or three verified boards so you can cross-reference the extracted fields directly against the live career pages before adding broad keyword filters.&lt;/p&gt;

&lt;p&gt;If an employer does not use Greenhouse or Lever, omit them from this configuration. Avoid guessing slugs, as invalid tokens produce source errors that complicate your monitoring logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Inspect a public source response
&lt;/h2&gt;

&lt;p&gt;Before scheduling recurring runs, inspect the raw data the ATS returns. You can verify how the &lt;strong&gt;Lever postings API&lt;/strong&gt; handles &lt;a href="https://www.eunit.me/blog/tags/Data%20Extraction" rel="noopener noreferrer"&gt;data extraction&lt;/a&gt; using this quick &lt;a href="https://eunit.me/project/tags/Python" rel="noopener noreferrer"&gt;Python&lt;/a&gt; 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;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;endpoint&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://api.lever.co/v0/postings/leverdemo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&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;client&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;client&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;endpoint&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;mode&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;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;skip&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
 &lt;span class="n"&gt;jobs&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="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hostedUrl&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;While this snippet requests just two records to preview the schema, a complete monitoring run cannot stop at two. Lever paginates results using &lt;code&gt;skip&lt;/code&gt; and &lt;code&gt;limit&lt;/code&gt;, and the Actor automatically follows pagination until the entire catalog is retrieved before running comparisons.&lt;/p&gt;

&lt;p&gt;For Greenhouse boards, the Actor fetches the full job listing and compares the number of received records against any summary totals provided by the API. If an ATS endpoint responds with data exceeding safety limits (such as a 10 MB payload cap or 5,000 jobs per board), the Actor flags the fetch as incomplete rather than risking an incorrect baseline comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Run a bounded snapshot
&lt;/h2&gt;

&lt;p&gt;To verify how the Actor processes and normalizes real records, execute a small snapshot run in Apify Console:&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boards"&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lever"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boardToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"leverdemo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"companyLabel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lever demo"&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;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"snapshot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"includeDescriptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;Notice the distinction between data limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;maxItems&lt;/code&gt; sets a cap on the number of result records saved to the default dataset.&lt;/li&gt;
&lt;li&gt;It does &lt;strong&gt;not&lt;/strong&gt; truncate the source fetch. The Actor still inspects the full board so that completeness rules remain intact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once the run finishes, check the default &lt;strong&gt;Dataset&lt;/strong&gt; tab, along with the &lt;code&gt;RUN-SUMMARY&lt;/code&gt; and &lt;code&gt;ERRORS&lt;/code&gt; records in the &lt;strong&gt;Key-value store&lt;/strong&gt; tab:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dataset&lt;/strong&gt;: Contains clean records with stable IDs, titles, and valid job URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RUN-SUMMARY&lt;/strong&gt;: Confirms total jobs observed, active board counts, and execution metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ERRORS&lt;/strong&gt;: Documents any non-fatal network or parsing problems without generating billable result records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each board is protected by sensible bounds: a 90-second fetch timeout, a 5,000-job per-board ceiling, and a 10 MB response limit. These safeguards prevent unexpected runaway runs on massive enterprise boards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Create a reusable baseline
&lt;/h2&gt;

&lt;p&gt;To track changes across time, set &lt;code&gt;mode&lt;/code&gt; to &lt;code&gt;changes&lt;/code&gt; and define a &lt;code&gt;stateStoreName&lt;/code&gt;. This tells the Actor which named Key-Value store to use for preserving baseline snapshots between runs.&lt;/p&gt;

&lt;p&gt;When monitoring an active company for the first time, you usually do not want 200 immediate notifications announcing every pre-existing vacancy as "new." Use &lt;code&gt;baselineOnly: true&lt;/code&gt; to perform a silent initial sync:&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boards"&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="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lever"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boardToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"leverdemo"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"changes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stateStoreName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-employer-watchlist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"baselineOnly"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"includeDescriptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;baselineOnly: true&lt;/code&gt;, the Actor ingests and saves the complete matching catalog into your named Key-Value store without emitting rows to the dataset. You establish a clean starting baseline without triggering notification noise or custom result charges.&lt;/p&gt;

&lt;p&gt;The state key generated internally incorporates the provider, board token, active keyword filters, and description settings. If you later change these core filters, the Actor treats the run as a new comparison scope to avoid invalid diffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Compare the next observation
&lt;/h2&gt;

&lt;p&gt;Once your baseline is established, set &lt;code&gt;baselineOnly&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; for all subsequent runs using the same &lt;code&gt;stateStoreName&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If no openings were created, modified, or removed since the previous run, a successful execution emits &lt;strong&gt;zero rows&lt;/strong&gt;. In job change monitoring, zero emitted rows is an ideal outcome: it proves the board was checked completely and produced no false positives.&lt;/p&gt;

&lt;p&gt;When changes do occur, the Actor outputs specific event types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title or attribute updates&lt;/strong&gt;: Emits an &lt;code&gt;updated&lt;/code&gt; record with &lt;code&gt;changed_fields&lt;/code&gt; detailing what was modified (e.g., &lt;code&gt;["title", "location"]&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Description modifications&lt;/strong&gt;: When description tracking is enabled, the Actor checks a cryptographic hash of the normalized description text. It catches edits to job requirements even if you truncate long descriptions to save storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial result advancement&lt;/strong&gt;: If your &lt;code&gt;maxItems&lt;/code&gt; cap is reached during a high-activity run, state advances only for the events successfully written to the dataset. Unemitted changes are preserved and processed during the next run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At-least-once delivery&lt;/strong&gt;: State updates and dataset writes occur in distinct steps. In the rare event of a network interruption between them, a change might be re-emitted on the subsequent run. Downstream systems should deduplicate events using the composite &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;observed_at&lt;/code&gt; fields.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 7: Add filters deliberately
&lt;/h2&gt;

&lt;p&gt;Refine your feed using case-insensitive keyword filters for job titles and locations:&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boards"&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="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"greenhouse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"boardToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_VERIFIED_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"changes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"stateStoreName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"engineering-watchlist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"titleKeywords"&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;"engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"developer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"locationKeywords"&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;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"includeDescriptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"includeRemoved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&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;Keep your filter strategy focused:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title filters&lt;/strong&gt;: Match substrings within the role title. A filter for &lt;code&gt;engineer&lt;/code&gt; catches both "Frontend Engineer" and "Engineering Manager."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location filters&lt;/strong&gt;: Evaluate the raw location text provided by the ATS. Keep in mind that labels like "Remote" or "US Remote" don't confirm legal work eligibility or state-specific tax compliance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managing removals (&lt;code&gt;includeRemoved&lt;/code&gt;)&lt;/strong&gt;: When set to &lt;code&gt;true&lt;/code&gt;, roles that disappear from complete board fetches emit &lt;code&gt;removed-from-board&lt;/code&gt; events. If you set this to &lt;code&gt;false&lt;/code&gt;, removed jobs are silently retired from baseline state without cluttering your destination feed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 8: Automate with the Apify client
&lt;/h2&gt;

&lt;p&gt;You can automate recurring checks and build reliable &lt;a href="https://www.eunit.me/blog/tags/Workflow%20Automation" rel="noopener noreferrer"&gt;workflow automation&lt;/a&gt; using the &lt;a href="https://eunit.me/project/tags/Apify%20SDK" rel="noopener noreferrer"&gt;Apify SDK&lt;/a&gt;. Store your API token and Actor ID in environment variables rather than hardcoding them in source files:&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;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&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;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;run&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;actor&lt;/span&gt;&lt;span class="p"&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;ATS_ACTOR_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boards&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;provider&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;lever&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;boardToken&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;leverdemo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mode&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;changes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stateStoreName&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;my-employer-watchlist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;baselineOnly&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="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;run_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;memory_mbytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_total_charge_usd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.25&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;run&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="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Actor run was not found.&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;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUCCEEDED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Inspect the failed run before using its observations&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;change&lt;/span&gt; &lt;span class="ow"&gt;in&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;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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;change&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;change_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;change&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;change&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;job_url&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;h3&gt;
  
  
  Scheduling tips
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Choose a practical cadence&lt;/strong&gt;: For recruitment research or competitive analysis, once or twice daily is typically ideal. Tech companies rarely publish or withdraw job requisitions by the minute. Running every hour increases compute overhead without surfacing meaningful hiring velocity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prevent run collisions&lt;/strong&gt;: Avoid scheduling overlapping runs that target the same &lt;code&gt;stateStoreName&lt;/code&gt;. Ensure previous runs finish before starting the next cycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 9: Deliver a traceable report
&lt;/h2&gt;

&lt;p&gt;When sending change feeds downstream to Slack (similar to our architecture for &lt;a href="https://www.eunit.me/blog/how-i-automated-hyper-local-lead-routing-with-nextdoor-scraper-and-slack" rel="noopener noreferrer"&gt;automated lead routing with Slack alerts&lt;/a&gt;), Notion (as explored in our guides on &lt;a href="https://www.eunit.me/blog/build-automated-seo-pipeline-sitemap-generator-and-google-indexer-to-notion" rel="noopener noreferrer"&gt;building automated pipelines connected to Notion&lt;/a&gt; and &lt;a href="https://www.eunit.me/blog/how-to-automate-instagram-reels-to-notion-via-mcp" rel="noopener noreferrer"&gt;automating content into Notion&lt;/a&gt;), Airtable, or a database, structure your records around traceable facts rather than assumptions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Maintain primary keys&lt;/strong&gt;: Use the composite &lt;code&gt;id&lt;/code&gt; (or &lt;code&gt;provider&lt;/code&gt; + &lt;code&gt;source_job_id&lt;/code&gt;) as your table's unique record identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve live links&lt;/strong&gt;: Always include the normalized &lt;code&gt;job_url&lt;/code&gt; in notifications so team members can verify the live posting with a single click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use accurate status labels&lt;/strong&gt;: Label departures as "Unpublished from public board" rather than "Candidate hired." If an employer renames a role, display the previous and updated values side by side using &lt;code&gt;changed_fields&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolate external sinks&lt;/strong&gt;: Treat webhook notifications and database synchronizations as independent consumer processes. If a downstream API experiences downtime, your Apify dataset preserves the raw records until your delivery workers recover.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Building a dependable &lt;a href="https://apify.com/eunit/ats-job-change-monitor" rel="noopener noreferrer"&gt;&lt;strong&gt;Greenhouse job change monitor&lt;/strong&gt;&lt;/a&gt; comes down to respecting source data boundaries: tracking persistent IDs, verifying complete board fetches, preserving baseline state during outages, and categorizing changes into unambiguous events.&lt;/p&gt;

&lt;p&gt;By combining public Greenhouse and Lever endpoints with the &lt;a href="https://apify.com/eunit/ats-job-change-monitor" rel="noopener noreferrer"&gt;&lt;strong&gt;ATS Job Change Monitor&lt;/strong&gt;&lt;/a&gt; on the Apify platform, you can automate clean, noise-free hiring intelligence feeds that your recruiting team, analysts, or job board users can trust.&lt;/p&gt;

&lt;p&gt;Start by verifying one or two company board tokens, seed a clean baseline with &lt;code&gt;baselineOnly: true&lt;/code&gt;, and test an unchanged follow-up run before connecting downstream automations.&lt;/p&gt;

&lt;p&gt;For more guides on production scrapers and web automation, explore our &lt;a href="https://www.eunit.me/blog/category/Technical" rel="noopener noreferrer"&gt;Technical Articles&lt;/a&gt; hub, including our hands-on &lt;a href="https://www.eunit.me/blog/tags/Python%20Web%20Scraping" rel="noopener noreferrer"&gt;Python web scraping&lt;/a&gt; tutorials.&lt;/p&gt;

</description>
      <category>hiringintelligence</category>
      <category>webscraping</category>
      <category>leverjobs</category>
      <category>apify</category>
    </item>
    <item>
      <title>How to scrape Google Search results in 2027</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Tue, 06 Oct 2026 11:51:22 +0000</pubDate>
      <link>https://dev.to/eunit/how-to-scrape-google-search-results-in-2027-4c5c</link>
      <guid>https://dev.to/eunit/how-to-scrape-google-search-results-in-2027-4c5c</guid>
      <description>&lt;h1&gt;
  
  
  How to scrape Google Search results in 2027
&lt;/h1&gt;

&lt;p&gt;Extracting structured data from Google search engine results pages (SERPs) has never been more important for competitive research, search engine optimization (SEO) tracking, and powering artificial intelligence (AI) applications. It has also become more challenging to do reliably.&lt;/p&gt;

&lt;p&gt;The classic "10 blue links" tell only part of the story. AI Overviews, local map packs, job cards, and Knowledge Graph panels can appear alongside organic results. Meanwhile, consent screens, traffic challenges, and changing page layouts can turn a working Google scraper into an empty dataset.&lt;/p&gt;

&lt;p&gt;In this article, you will learn how to scrape Google Search results with Python and prepare your workflow for 2027. We will examine three traditional DIY methods, break down where they become difficult to maintain, and walk through extracting structured search data using &lt;a href="https://apify.com/eunit/google-search-scraper" rel="noopener noreferrer"&gt;Google Search Scraper&lt;/a&gt; on the &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify platform&lt;/a&gt;. You will also learn how to process rich results, call a Standby search endpoint, and compare pay-per-event costs with running your own infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Google SERPs contain more than organic links&lt;/strong&gt;: AI Overviews, local results, and question panels can make search data useful for both rank tracking and content research. Which sections appear depends on the query and search context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DIY scraping has ongoing costs&lt;/strong&gt;: Direct HTTP requests cannot execute JavaScript, while headless browsers add browser management, resource usage, and selector maintenance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Custom Search is a migration concern&lt;/strong&gt;: The Custom Search JSON API is closed to new customers, and existing customers must transition by January 1, 2027. It is not a new integration option for a 2027 project. &lt;a href="https://developers.google.com/custom-search/v1/overview" rel="noopener noreferrer"&gt;Google's service notice&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A managed Actor simplifies delivery&lt;/strong&gt;: Google Search Scraper turns supported SearchApi.io responses into structured JSON on the Apify platform, with batch and Standby interfaces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understand the billing unit&lt;/strong&gt;: The updated pricing proposal charges per saved search page, including available requested rich sections. Check the live &lt;strong&gt;Pricing&lt;/strong&gt; tab for event rates, startup charges, and platform usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The state of Google SERPs in 2027
&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%2F46r2b00vcrt1gbiyy0pj.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%2F46r2b00vcrt1gbiyy0pj.png" alt="How to scrape Google Search results in 2027" width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To understand why a simple Google results scraper can fail, first look at what you are asking it to collect.&lt;/p&gt;

&lt;p&gt;An older tutorial might send an HTTP GET request to &lt;code&gt;google.com/search?q=your+query&lt;/code&gt;, parse the HTML with BeautifulSoup or lxml, and select links using classes such as &lt;code&gt;.r &amp;gt; a&lt;/code&gt; or &lt;code&gt;div.g&lt;/code&gt;. That is a useful introduction to HTML parsing, but it leaves you responsible for recognizing the right page, handling different layouts, and finding the data your application needs.&lt;/p&gt;

&lt;p&gt;For a workflow you want to keep using in 2027, three changes deserve particular attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The rise of AI Overviews and dynamic components
&lt;/h3&gt;

&lt;p&gt;Google Search can present a generated answer alongside links to websites. For a content researcher, that introduces another question: is your competitor merely ranking in organic results, or is its content also cited in an AI Overview?&lt;/p&gt;

&lt;p&gt;An AI Overview can include summary text and links to supporting sources. Other sections, such as People Also Ask (PAA), can reveal follow-up questions that a basic list of titles and URLs would miss. Google explains that AI Overviews appear when its systems determine they will be helpful; they are not guaranteed for every informational or commercial query. &lt;a href="https://developers.google.com/search/docs/appearance/ai-features" rel="noopener noreferrer"&gt;Google's explanation of AI features in Search&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Some content may be available in the initial HTML, while other details require rendering or interaction. Downloading a page successfully does not prove that you captured everything visible in a browser. Your extraction workflow needs to distinguish an absent section from one it could not collect.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Aggressive anti-bot and fingerprinting infrastructure
&lt;/h3&gt;

&lt;p&gt;A search request also has a network and browser context. When an automated request receives different content from an interactive browser, investigate several layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP and traffic context&lt;/strong&gt;: Shared networks, request frequency, and simultaneous requests can affect whether you receive results or a traffic challenge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transport Layer Security (TLS) behavior&lt;/strong&gt;: A Python HTTP client and a browser establish connections differently. Copying a browser's &lt;code&gt;User-Agent&lt;/code&gt; header does not make their connection behavior identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP protocol behavior&lt;/strong&gt;: Headers, redirects, cookies, and connection settings can change what a server returns. These are useful debugging clues, rather than proof of a particular private detection rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser and session state&lt;/strong&gt;: JavaScript execution, consent choices, and session cookies can affect the page you receive and the content available to extract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Google documents that automated traffic can trigger its &lt;a href="https://support.google.com/websearch/answer/86640" rel="noopener noreferrer"&gt;unusual-traffic warning&lt;/a&gt;. A successful HTTP status alone is therefore not enough: a response may contain a consent page or challenge instead of search results. Validate the page content before recording a scrape as successful, and handle access restrictions rather than repeatedly retrying them.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Rapidly shifting and obfuscated DOM structures
&lt;/h3&gt;

&lt;p&gt;The document object model (DOM) describes a page's structure. A selector such as &lt;code&gt;div.g&lt;/code&gt; depends on that structure staying consistent, but Google layouts can differ by device, query, region, and experiment.&lt;/p&gt;

&lt;p&gt;You may encounter generated class names such as &lt;code&gt;VwiC3b&lt;/code&gt; or different containers around a result heading. A selector that extracts the title from one layout can return nothing, duplicate links, or the wrong parent element in another.&lt;/p&gt;

&lt;p&gt;This is a particularly frustrating failure mode because your script may finish without an exception. For an SEO pipeline, an empty list should trigger an investigation, not automatically become a report that a competitor has disappeared from Search.&lt;/p&gt;

&lt;h2&gt;
  
  
  What data can you extract from modern Google SERPs?
&lt;/h2&gt;

&lt;p&gt;Before writing code, it is helpful to inventory the data surfaces available across a modern SERP. The fields you need depend on your business question. These are the eight result categories handled by the Actor, with fields returned where available:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result type&lt;/th&gt;
&lt;th&gt;Extracted fields&lt;/th&gt;
&lt;th&gt;Common business use cases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Organic results&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Position, title, URL, displayed URL, text snippet, highlighted keywords, site links, rating, and date&lt;/td&gt;
&lt;td&gt;SEO rank tracking, competitor page discovery, and brand visibility audits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI Overviews&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Returned summary text, formatted Markdown, and source citation links&lt;/td&gt;
&lt;td&gt;Generative engine optimization (GEO), AI citation analysis, and competitive messaging research&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Knowledge Graph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Entity title, entity type, description, official website, phone, attributes, and social profiles&lt;/td&gt;
&lt;td&gt;Entity authority mapping, knowledge base population, and company profiling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local results (Maps pack)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Business title, physical address, review score, total reviews, phone number, operating hours, and listing link&lt;/td&gt;
&lt;td&gt;Local SEO tracking, brick-and-mortar market research, and lead generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Jobs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Job title, hiring organization, work location, posting source, description, and available job highlights&lt;/td&gt;
&lt;td&gt;Recruitment intelligence, labor market trend analysis, and competitor hiring alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Events&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Event name, date, venue, physical location, listing links, and descriptive summary&lt;/td&gt;
&lt;td&gt;Event aggregation, ticketing platforms, and regional entertainment intelligence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;People Also Ask&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Question text, answer summary, source title, and destination link&lt;/td&gt;
&lt;td&gt;Content brief ideation, FAQ generation, and question-intent keyword clustering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Related searches&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Search suggestion term and destination search query link&lt;/td&gt;
&lt;td&gt;Semantic keyword expansion, topic clustering, and consumer search intent mapping&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The 3 traditional ways to scrape Google Search (and why they break)
&lt;/h2&gt;

&lt;p&gt;Developers building a Google SERP scraper typically follow one of three paths. Let us analyze each option with Python code to examine where they break in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 1: Google Custom Search JSON API
&lt;/h3&gt;

&lt;p&gt;Google's Custom Search JSON API returns structured web or image results from a configured Programmable Search Engine. It has often appeared in Python tutorials because it avoids writing an HTML parser.&lt;/p&gt;

&lt;p&gt;There is an important availability change: &lt;strong&gt;the API is closed to new customers&lt;/strong&gt;, and Google gives existing customers until &lt;strong&gt;January 1, 2027&lt;/strong&gt; to transition to an alternative. The following example helps explain an existing integration during that transition; do not use it as the starting point for a new 2027 service. &lt;a href="https://developers.google.com/custom-search/v1/overview" rel="noopener noreferrer"&gt;Google's Custom Search documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An existing customer with an API key and search engine ID can recognize the request pattern below. Install &lt;code&gt;requests&lt;/code&gt; with &lt;code&gt;pip install requests&lt;/code&gt; if needed:&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;requests&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;GOOGLE_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;SEARCH_ENGINE_ID&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;GOOGLE_CSE_ID&lt;/span&gt;&lt;span class="sh"&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;query_google_custom_search&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start_index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="o"&gt;-&amp;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;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.googleapis.com/customsearch/v1&lt;/span&gt;&lt;span class="sh"&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;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;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SEARCH_ENGINE_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;q&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;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start_index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&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;10&lt;/span&gt;&lt;span class="p"&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;raise_for_status&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;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="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;query_google_custom_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;best web scraping tools 2027&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;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;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="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="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Why it breaks in production: Google Custom Search JSON API
&lt;/h4&gt;

&lt;p&gt;The main issue for a new project is availability, followed by whether the returned data answers your question:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A transition deadline&lt;/strong&gt;: Existing integrations need a replacement by the announced date. New customers cannot obtain access to this API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quotas during the transition&lt;/strong&gt;: For eligible existing customers, Google documents 100 free queries per day, then $5 per 1,000 additional queries, up to 10,000 queries per day. Those are legacy service terms, not a new 2027 offer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A different search product&lt;/strong&gt;: Programmable Search Engine results are not a capture of a &lt;code&gt;google.com&lt;/code&gt; results page. Do not assume identical rankings or the same AI Overview, local pack, and question-panel coverage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an application that searches a defined collection of sites, evaluate a supported search service suited to that task. For an SEO tool observing Google SERPs, evaluate the actual result fields and localization support instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Direct HTTP requests with BeautifulSoup or Parsel
&lt;/h3&gt;

&lt;p&gt;The second common approach is sending direct HTTP GET requests to &lt;code&gt;google.com/search&lt;/code&gt; using an HTTP client like &lt;code&gt;httpx&lt;/code&gt; or &lt;code&gt;requests&lt;/code&gt; and parsing the resulting HTML with BeautifulSoup or Parsel. To try this small parsing example, install its dependencies with &lt;code&gt;pip install "httpx[http2]" parsel&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;httpx&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;parsel&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Selector&lt;/span&gt;

&lt;span class="n"&gt;headers&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;User-Agent&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;Mozilla/5.0 (Windows NT 10.0; Win64; x64) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AppleWebKit/537.36 (KHTML, like Gecko) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chrome/130.0.0.0 Safari/537.36&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;Accept-Language&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;en-US,en;q=0.9&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;Accept&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;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8&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;def&lt;/span&gt; &lt;span class="nf"&gt;scrape_google_serp&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&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;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.google.com/search&lt;/span&gt;&lt;span class="sh"&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;q&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;hl&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;en&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;gl&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;us&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&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;http2&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;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;follow_redirects&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;20.0&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;client&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;client&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="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unusual traffic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&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="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="ow"&gt;or&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;429&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Request was blocked by Google anti-bot systems.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Selector&lt;/span&gt;&lt;span class="p"&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="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="p"&gt;[]&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xpath&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[.//h3]&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="o"&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;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.//h3//text()&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getall&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&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;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xpath&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="nf"&gt;get&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;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xpath&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/@href&lt;/span&gt;&lt;span class="sh"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="ow"&gt;and&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;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;http&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="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;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="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;link&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;results&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="nf"&gt;scrape_google_serp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python web scraping tutorial&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;Extracted &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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&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;Scraping failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&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;h4&gt;
  
  
  Why it breaks in production: direct HTTP requests
&lt;/h4&gt;

&lt;p&gt;This example illustrates how to collect heading links from a returned document. It is not a complete Google SERP parser, and an empty result is not proof that Google has no matching pages.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blocked or unexpected responses&lt;/strong&gt;: A request can receive a rate limit, challenge, consent screen, or other content. There is no universal number of requests after which every IP is blocked. Inspect the response and stop when access is restricted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No JavaScript execution&lt;/strong&gt;: An HTTP client cannot render or interact with the page. Content loaded after the initial response may be missing, even when some rich results are already present in the HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selector maintenance&lt;/strong&gt;: A heading selector does not reliably distinguish organic results from every other linked heading. Snippets, result positions, local cards, and citation panels need additional parsing and validation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Direct requests can be useful for learning or a small experiment. The workload grows when you need consistent coverage across hundreds of keywords, different devices, and repeated collection dates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Headless browsers with Playwright or Selenium
&lt;/h3&gt;

&lt;p&gt;To overcome static HTML limitations, many teams turn to headless browser automation using Playwright, Puppeteer, or Selenium. By spinning up a full Chromium instance, the script executes Google's client-side JavaScript, renders fonts, and can interact with the page. Install Playwright with &lt;code&gt;pip install playwright&lt;/code&gt;, then install its browser with &lt;code&gt;playwright install chromium&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;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;urlencode&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scrape_with_playwright&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&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="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&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;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headless&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="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;viewport&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;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&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="nf"&gt;urlencode&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="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;gl&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;us&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;hl&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;en&lt;/span&gt;&lt;span class="sh"&gt;"&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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://www.google.com/search?&lt;/span&gt;&lt;span class="sh"&gt;"&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;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&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;30_000&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;response&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Search navigation did not succeed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# A page can load successfully without containing search results.
&lt;/span&gt;            &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;inner_text&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;10_000&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sorry/index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&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;unusual traffic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Encountered a Google traffic challenge.&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;consent.google.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Consent is required before collecting results.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&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 h3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&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;15_000&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="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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&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 h3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;xpath=ancestor::a[1]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get_attribute&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="ow"&gt;and&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;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;http&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="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;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="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;link&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;results&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nf"&gt;scrape_with_playwright&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python web scraping tutorial&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;Found &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;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; heading links with Playwright&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;For more background on browser automation, see my &lt;a href="https://www.eunit.me/blog/playwright-vs-webdriverio-uncovering-the-differences" rel="noopener noreferrer"&gt;comparison of Playwright and WebdriverIO&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why it breaks in production: headless browsers
&lt;/h4&gt;

&lt;p&gt;A browser gives you JavaScript execution, but it also gives you a browser fleet to operate. The example above still collects heading links, rather than a complete structured SERP.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resource consumption&lt;/strong&gt;: Browser processes use CPU and memory. The amount depends on the page, browser settings, and concurrency, so measure your workload before choosing a server size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Challenges and session handling&lt;/strong&gt;: Opening Chromium does not guarantee access. Your workflow still needs to recognize consent screens, rate limits, and challenges, then handle them appropriately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network and operational costs&lt;/strong&gt;: Depending on the source and permitted collection method, you may need network infrastructure, monitoring, and retry controls. Include those expenses and failed attempts in your estimate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selector upkeep&lt;/strong&gt;: Rendering a widget does not automatically turn it into structured data. You still need extraction logic for AI Overviews, Knowledge Graphs, job listings, and their changing layouts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you enjoy building and maintaining that infrastructure, a browser-based approach gives you control. If your goal is to analyze search data or power an application, a managed search-data interface can reduce the work between a query and a useful dataset.&lt;/p&gt;

&lt;h2&gt;
  
  
  The modern solution: Google Search Scraper on the Apify platform
&lt;/h2&gt;

&lt;p&gt;Instead of building, debugging, and hosting the collection infrastructure yourself, you can use a managed cloud Actor designed for search data. An &lt;a href="https://docs.apify.com/actors" rel="noopener noreferrer"&gt;Actor&lt;/a&gt; is an application that runs on the Apify platform and can be called from Apify Console, an API, or your own 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%2Fycdd9hv4c743edfqyns9.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%2Fycdd9hv4c743edfqyns9.png" alt="Google Search Scraper on Apify" width="799" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/google-search-scraper" rel="noopener noreferrer"&gt;Google Search Scraper&lt;/a&gt; bridges search queries and structured API delivery. The updated implementation calls SearchApi.io, normalizes the supported response fields, and saves batch results to an Apify dataset. It does not run its own Google HTML parser.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Version note: This walkthrough covers the updated Actor interface, which is in private testing at the time of writing. Use an authorized deployment to follow along until that version is available in the Store; older builds and prices can differ.&lt;/em&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%2Fl5fmolt8ng6yix1mgas1.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%2Fl5fmolt8ng6yix1mgas1.png" alt="Your application or pipeline" width="625" height="768"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The updated request flow is:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    App["Your application or pipeline"]
    Actor["Google Search Scraper on the Apify platform"]
    Provider["SearchApi.io"]
    Search["Google Search data"]
    Output["Batch dataset or Standby JSON response"]
    App --&amp;gt; Actor
    Actor --&amp;gt; Provider
    Provider --&amp;gt; Search
    Search --&amp;gt; Provider
    Provider --&amp;gt; Actor
    Actor --&amp;gt; Output&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Why this approach wins in 2027
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eight result categories in one response&lt;/strong&gt;: Collect organic results and available AI Overviews, Knowledge Graphs, local results, jobs, events, People Also Ask questions, and related searches. Each rich section depends on what the provider returns for that query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No customer proxy setup&lt;/strong&gt;: When you use the hosted Actor, you do not configure a browser fleet or a proxy rotation pool. SearchApi.io supplies the upstream search-data service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location and device controls&lt;/strong&gt;: Set a country (&lt;code&gt;gl&lt;/code&gt;), interface language (&lt;code&gt;hl&lt;/code&gt;), and device. The batch interface also supports city or regional targeting through &lt;code&gt;location&lt;/code&gt;, helping you compare like-for-like search observations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A clear page-based event&lt;/strong&gt;: The updated pay-per-event (PPE) proposal uses one saved search page as its custom billing unit. Available requested AI Overview and Knowledge Graph sections are included, rather than charged as separate custom add-ons. Startup and usage charges still depend on the live pricing configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch and Standby modes&lt;/strong&gt;: Use batch runs for keyword collections and recurring audits, or request a JSON response through the Standby endpoint for an application or AI agent. Response time depends on the upstream request and deployment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the hosted Actor, the operator provisions the SearchApi.io credentials. If you deploy your own copy, configure &lt;code&gt;SEARCHAPI_IO_KEY&lt;/code&gt; as an environment secret and budget for that provider separately. Your Apify token authenticates calls to Apify; it is not a replacement for the operator's upstream key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-step tutorial: scraping Google Search with Python
&lt;/h2&gt;

&lt;p&gt;Let us walk through a complete, hands-on tutorial using the &lt;a href="https://docs.apify.com/api/client/python/reference/class/ActorClient" rel="noopener noreferrer"&gt;official Apify Python client&lt;/a&gt; to extract structured SERP data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install the Apify client
&lt;/h3&gt;

&lt;p&gt;In your Python terminal or virtual environment, install the &lt;code&gt;apify-client&lt;/code&gt; package:&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; &lt;span class="s2"&gt;"apify-client&amp;gt;=3.3,&amp;lt;4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Retrieve your Apify API token
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Log in to &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify Console&lt;/a&gt;. If you do not have an account, sign up and check the current free allowance and account pricing before running the example.&lt;/li&gt;
&lt;li&gt;Open the &lt;a href="https://console.apify.com/settings/integrations" rel="noopener noreferrer"&gt;&lt;strong&gt;API &amp;amp; Integrations&lt;/strong&gt; page&lt;/a&gt; in &lt;strong&gt;Settings&lt;/strong&gt; to find your API tokens.&lt;/li&gt;
&lt;li&gt;Copy your personal API token and set it in your local environment:
&lt;/li&gt;
&lt;/ol&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;APIFY_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_apify_token_here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use PowerShell on Windows, set it for the current session with:&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="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your_apify_token_here"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the token out of source control and client-side application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run the Actor and process results
&lt;/h3&gt;

&lt;p&gt;Create a file named &lt;code&gt;scrape_google.py&lt;/code&gt; and add the following Python code. Start with two queries and one page each so you can inspect the output before increasing the volume. For a private deployment, set &lt;code&gt;GOOGLE_SEARCH_ACTOR_ID&lt;/code&gt; to its Actor ID or &lt;code&gt;username/actor-name&lt;/code&gt;; the default below is the Store identifier.&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;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize the Apify client with your API token
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&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;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;actor_id&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;GOOGLE_SEARCH_ACTOR_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;eunit/google-search-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Configure your search parameters
&lt;/span&gt;&lt;span class="n"&gt;actor_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;# Provide one or more search queries
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;queries&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;Apify documentation&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;best telehealth platforms&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="c1"&gt;# Number of result pages per query; the organic result count can vary
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_pages_per_query&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="c1"&gt;# Geographic country code and interface language
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gl&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;us&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;hl&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;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Request desktop, mobile, or tablet search results
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;device&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;desktop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# SafeSearch filter: "active", "blur", or "off"
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;safe&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;blur&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Select which rich data surfaces to extract
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_ai_overview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_knowledge_graph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_jobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_local_results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_related_questions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_related_searches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Concurrency control
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_concurrency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting Google Search Scraper on the Apify platform...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Execute the Actor run and wait for it to complete
&lt;/span&gt;&lt;span class="n"&gt;run&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;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor_input&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;run&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Actor run was not found.&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;Run completed with status: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&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;Dataset ID: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Fetch and iterate through the structured items in the default dataset
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUCCEEDED&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Actor run did not succeed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Check its log.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;dataset&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;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_dataset_id&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;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query_term&lt;/span&gt; &lt;span class="o"&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;searchQuery&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;term&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;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;error&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;Search failed for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query_term&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="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="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;continue&lt;/span&gt;
    &lt;span class="n"&gt;total_results&lt;/span&gt; &lt;span class="o"&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;searchInformation&lt;/span&gt;&lt;span class="sh"&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;totalResults&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;total_results&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;total_results&lt;/span&gt; &lt;span class="o"&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="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="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&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;=&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;Search query: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query_term&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (Reported result estimate: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_results&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&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="c1"&gt;# 1. AI Overview
&lt;/span&gt;    &lt;span class="k"&gt;if&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;aiOverview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[AI Overview]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ai_text&lt;/span&gt; &lt;span class="o"&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;aiOverview&lt;/span&gt;&lt;span class="sh"&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;text&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="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;ai_text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;300&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="k"&gt;if&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;ai_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;ai_text&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;Citations:&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;ref&lt;/span&gt; &lt;span class="ow"&gt;in&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;aiOverview&lt;/span&gt;&lt;span class="sh"&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;referenceLinks&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ref&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="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;ref&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="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Knowledge Graph
&lt;/span&gt;    &lt;span class="k"&gt;if&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;knowledgeGraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;kg&lt;/span&gt; &lt;span class="o"&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;knowledgeGraph&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[Knowledge Graph: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kg&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="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;kg&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;kg&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;description&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;Description: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&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;if&lt;/span&gt; &lt;span class="n"&gt;kg&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;website&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;Website: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;kg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;website&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="c1"&gt;# 3. Organic Results
&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[Organic Results: &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;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;organicResults&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; found]&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;org&lt;/span&gt; &lt;span class="ow"&gt;in&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;organicResults&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;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;org&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;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;org&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;org&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pos&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;title&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;     URL: &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="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;org&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;description&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;     Snippet: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;120&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="c1"&gt;# 4. People Also Ask
&lt;/span&gt;    &lt;span class="k"&gt;if&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;relatedQuestions&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[People Also Ask: &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;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;relatedQuestions&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; questions]&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;q&lt;/span&gt; &lt;span class="ow"&gt;in&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;relatedQuestions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  ? &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;question&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;if&lt;/span&gt; &lt;span class="n"&gt;q&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;answer&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;    Ans: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;100&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="c1"&gt;# 5. Related Searches
&lt;/span&gt;    &lt;span class="k"&gt;if&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;relatedSearches&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[Related Searches]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;related&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;r&lt;/span&gt; &lt;span class="ow"&gt;in&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;relatedSearches&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&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; | &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;related&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Inspecting the output data
&lt;/h3&gt;

&lt;p&gt;Run the script with &lt;code&gt;python scrape_google.py&lt;/code&gt;. After the run succeeds, it reads the default dataset and prints the query, organic results, and any available rich sections.&lt;/p&gt;

&lt;p&gt;Here is an excerpt from an actual test for &lt;code&gt;Apify documentation&lt;/code&gt; on October 21, 2026. The arrays and organic result fields are shortened for readability; the values below come from that saved observation:&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;"searchQuery"&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;"term"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Apify documentation"&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://www.google.com/search?q=Apify documentation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"desktop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"page"&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;"gl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&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;"searchInformation"&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;"queryDisplayed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Apify documentation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalResults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;340000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timeTakenDisplayed"&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.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"didYouMean"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"detectedLocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"95240, California"&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;"organicResults"&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;"Apify Documentation"&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://docs.apify.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"displayedUrl"&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://docs.apify.com"&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;"Apify is all about Actors&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;2014a new way to package your code to make it easy to share, integrate, and build upon. Actors on the Apify ..."&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;"Apify: Marketplace of ready-to-run tools for AI"&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://apify.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"displayedUrl"&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://apify.com"&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;"Thousands of tools to automate your business. Get real-time web data, track competitors, generate leads, and integrate your apps and AI agents."&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;"relatedQuestions"&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;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What is Apify mainly used for?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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="kc"&gt;null&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;"relatedSearches"&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;"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;"Apify documentation examples"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.google.com/search?sca_esv=9dd08dafeeb6e81c&amp;amp;gl=us&amp;amp;hl=en&amp;amp;pws=0&amp;amp;sxsrf=APpeQnvn1FNcy-Pkd1swx5pHUqvUPm7Azg:1792623169968&amp;amp;q=Apify+documentation+examples&amp;amp;sa=X&amp;amp;ved=2ahUKEwjc1YvamcyXAxXZHoAAHVM9DpcQ1QJ6BAgqEAE"&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;"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;"Apify login"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.google.com/search?sca_esv=9dd08dafeeb6e81c&amp;amp;gl=us&amp;amp;hl=en&amp;amp;pws=0&amp;amp;sxsrf=APpeQnvn1FNcy-Pkd1swx5pHUqvUPm7Azg:1792623169968&amp;amp;q=Apify+login&amp;amp;sa=X&amp;amp;ved=2ahUKEwjc1YvamcyXAxXZHoAAHVM9DpcQ1QJ6BAg-EAE"&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;"localResults"&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;"jobs"&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;"events"&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;"aiOverview"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"knowledgeGraph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scrapedAt"&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-10-21T22:52:51.501393+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page"&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;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;Notice that &lt;code&gt;aiOverview&lt;/code&gt; and &lt;code&gt;knowledgeGraph&lt;/code&gt; are &lt;code&gt;null&lt;/code&gt; in this example. Requesting those sections makes the Actor include them when available; it does not make Google generate them. Similarly, an empty jobs or events array can be normal for a query that is not about jobs or events.&lt;/p&gt;

&lt;p&gt;The returned &lt;code&gt;totalResults&lt;/code&gt; is a reported estimate, not a count of records in your dataset. Keep &lt;code&gt;searchQuery&lt;/code&gt;, &lt;code&gt;page&lt;/code&gt;, and &lt;code&gt;scrapedAt&lt;/code&gt; with your results so you can compare observations collected under the same settings.&lt;/p&gt;

&lt;p&gt;You can export a batch dataset from Apify Console in JSON, CSV, Excel, XML, or HTML table format. JSON is useful for nested citations and site links; a spreadsheet is convenient for sorting rankings or reviewing content ideas. Integrations and completion webhooks can connect the run to your wider workflow.&lt;/p&gt;

&lt;p&gt;For recurring SEO monitoring, save the input as a task and schedule it. Each run creates its own dataset, so use that run's dataset ID when importing results into your database or spreadsheet. A saved task does not automatically append every observation to one permanent search-history table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Standby mode: turning Google into a real-time HTTP search API
&lt;/h2&gt;

&lt;p&gt;Batch scraping is ideal when you need to process hundreds or thousands of keywords for weekly SEO audits. Applications can also need search results on demand - for example, AI agents researching a question, live chat assistants retrieving citations, or retrieval-augmented generation (RAG) pipelines.&lt;/p&gt;

&lt;p&gt;Starting a separate batch run for each question adds startup overhead. To avoid repeating that pattern, &lt;a href="https://apify.com/eunit/google-search-scraper" rel="noopener noreferrer"&gt;Google Search Scraper&lt;/a&gt; supports &lt;strong&gt;Standby mode&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Standby mode works
&lt;/h3&gt;

&lt;p&gt;In Standby mode, the Apify platform keeps the Actor ready to receive HTTP requests, allowing it to behave like an API server. Instead of starting a new batch job for every query, your application calls the &lt;code&gt;/search&lt;/code&gt; endpoint and receives structured JSON.&lt;/p&gt;

&lt;p&gt;Open the Actor's &lt;strong&gt;Endpoints&lt;/strong&gt; tab and copy the deployed hostname. Set that base URL as &lt;code&gt;SEARCH_STANDBY_URL&lt;/code&gt; in your environment, without appending &lt;code&gt;/search&lt;/code&gt;. If the tab is absent, Standby is not enabled for that version. Authenticate with your Apify token in the &lt;code&gt;Authorization: Bearer&lt;/code&gt; header, as described in the &lt;a href="https://docs.apify.com/actors/running/standby" rel="noopener noreferrer"&gt;Apify Standby documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Standby reduces the need to create individual batch runs, but the upstream search still takes time. Benchmark your own query mix before choosing application timeouts or promising a response-time target.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standby API example in Python
&lt;/h3&gt;

&lt;p&gt;You can call the endpoint with a standard HTTP client. Install &lt;code&gt;httpx&lt;/code&gt; with &lt;code&gt;pip install httpx&lt;/code&gt; if you did not install it for the earlier example. Keep the API token on your server, rather than exposing it in browser JavaScript.&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;httpx&lt;/span&gt;

&lt;span class="c1"&gt;# Copy the base hostname from the Actor's Endpoints tab.
&lt;/span&gt;&lt;span class="n"&gt;endpoint&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;SEARCH_STANDBY_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;rstrip&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="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;headers&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;Authorization&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;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&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;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="c1"&gt;# Standby uses URL parameters, rather than the batch input's queries list.
&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;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;best telehealth startups Nigeria&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;gl&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;ng&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;hl&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;en&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;device&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;desktop&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;page&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_ai_overview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;45.0&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;client&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;client&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;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;serp&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;if&lt;/span&gt; &lt;span class="n"&gt;serp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Search failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;serp&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="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;Scraped term: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;serp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;searchQuery&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;term&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;Total organic results: &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;serp&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;organicResults&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="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;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;serp&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;organicResults&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;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;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;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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="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="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;if&lt;/span&gt; &lt;span class="n"&gt;serp&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;aiOverview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AI Overview:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;serp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aiOverview&lt;/span&gt;&lt;span class="sh"&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;text&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;This interface is useful when an AI agent needs a few current sources for a question, while batch mode is more convenient for a collection you want to export and compare later. Standby also incurs platform usage, so include it in the cost of an application that sends frequent requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost comparison: DIY infrastructure vs. pay-per-event pricing
&lt;/h2&gt;

&lt;p&gt;When choosing how to scrape Google Search results, compare the cost of obtaining useful data, rather than only the price of one successful request. A DIY workflow has operating and maintenance expenses; a managed Actor has event charges and potentially separate usage charges.&lt;/p&gt;

&lt;p&gt;The updated Actor's &lt;strong&gt;proposed&lt;/strong&gt; custom event is &lt;code&gt;search-page-scraped&lt;/code&gt; at &lt;strong&gt;$0.008 per saved search page&lt;/strong&gt;, or &lt;strong&gt;$8 per 1,000 pages&lt;/strong&gt;. A page can contain several organic links, so one event is not one individual website result. Available requested AI Overview and Knowledge Graph sections are included in that custom event.&lt;/p&gt;

&lt;p&gt;For a workload of &lt;strong&gt;50,000 successfully saved pages per month&lt;/strong&gt;, compare these components:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Expense component&lt;/th&gt;
&lt;th&gt;DIY Playwright workflow&lt;/th&gt;
&lt;th&gt;Updated Google Search Scraper proposal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Browser compute&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Budget for CPU, memory, browser concurrency, and failed attempts&lt;/td&gt;
&lt;td&gt;No customer browser fleet; Apify usage may be billed separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network and collection service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Budget for your chosen collection method and any network services&lt;/td&gt;
&lt;td&gt;SearchApi.io is provisioned by the hosted Actor's operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parsing and maintenance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Maintain selectors, layout handling, and monitoring&lt;/td&gt;
&lt;td&gt;The operator maintains normalization; you still validate fields and monitor your integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom result fee per 1,000 pages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No Actor event fee, but infrastructure and engineering still cost money&lt;/td&gt;
&lt;td&gt;Proposed &lt;strong&gt;$8&lt;/strong&gt; for saved-page events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom result fee for 50,000 pages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Calculate from measured infrastructure and maintenance costs&lt;/td&gt;
&lt;td&gt;Proposed &lt;strong&gt;$400&lt;/strong&gt;, before other applicable charges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Startup and Standby costs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Depend on how you host and scale the service&lt;/td&gt;
&lt;td&gt;Check startup events and platform usage in the live &lt;strong&gt;Pricing&lt;/strong&gt; tab&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The $400 calculation is a custom-event subtotal, not an all-inclusive invoice or an activated Store price. Apify supports PPE configurations that pass platform usage to the customer, and Standby usage is billed in addition to events. Review the actual configuration before running a large workload. &lt;a href="https://docs.apify.com/actors/monetize/pay-per-event" rel="noopener noreferrer"&gt;Apify's PPE billing documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the hosted Actor, you do not separately configure a SearchApi.io subscription. If you deploy your own copy, you become the operator and must include that provider's plan, request allowance, and overage costs in your estimate. For an Actor business, event revenue is also different from profit: deduct the platform's applicable share and any seller-paid operating costs before comparing prices.&lt;/p&gt;

&lt;p&gt;Start with a representative batch, inspect its output, and review the charges. That gives you a useful basis for forecasting a daily rank tracker, a weekly audit, or an on-demand search application without relying on unmeasured DIY cost ranges.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Is scraping Google Search legal?
&lt;/h3&gt;

&lt;p&gt;There is no single answer for every project or jurisdiction. Public visibility alone does not establish permission for every collection method or downstream use. Review &lt;a href="https://policies.google.com/terms" rel="noopener noreferrer"&gt;Google's terms&lt;/a&gt;, the applicable service and provider terms, and the rules that apply to the data you collect.&lt;/p&gt;

&lt;p&gt;For example, storing business rankings raises different questions from building a database of personal information found in search snippets. Define what you need, minimize unnecessary personal data, respect access restrictions, and seek qualified advice where the intended collection or reuse is uncertain.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does the scraper extract Google AI Overviews?
&lt;/h3&gt;

&lt;p&gt;The updated Actor calls SearchApi.io and normalizes any AI Overview returned in the provider response. It does not independently reconstruct an answer from the organic links.&lt;/p&gt;

&lt;p&gt;When available, &lt;code&gt;aiOverview.text&lt;/code&gt; holds the returned text, &lt;code&gt;aiOverview.markdown&lt;/code&gt; holds formatted content, and &lt;code&gt;aiOverview.referenceLinks&lt;/code&gt; contains citation information. Enable &lt;code&gt;include_ai_overview&lt;/code&gt; to request the section, then check whether it is present before processing it. Under the updated pricing proposal, an available AI Overview is included in the saved-page event.&lt;/p&gt;

&lt;p&gt;For citation research, save the query, language, country, device, and capture time alongside those references. A missing overview in one response does not prove that Google never shows one for that query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I localize queries to specific countries, languages, and cities?
&lt;/h3&gt;

&lt;p&gt;Yes. Use &lt;code&gt;gl&lt;/code&gt; for the country, such as &lt;code&gt;us&lt;/code&gt;, &lt;code&gt;gb&lt;/code&gt;, &lt;code&gt;de&lt;/code&gt;, or &lt;code&gt;ng&lt;/code&gt;, and &lt;code&gt;hl&lt;/code&gt; for the interface language, such as &lt;code&gt;en&lt;/code&gt;, &lt;code&gt;es&lt;/code&gt;, or &lt;code&gt;fr&lt;/code&gt;. Choose &lt;code&gt;desktop&lt;/code&gt;, &lt;code&gt;mobile&lt;/code&gt;, or &lt;code&gt;tablet&lt;/code&gt; through &lt;code&gt;device&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The batch interface also accepts &lt;code&gt;location&lt;/code&gt; for city or regional targeting, such as &lt;code&gt;"New York, United States"&lt;/code&gt; or &lt;code&gt;"Munich, Germany"&lt;/code&gt;. Use it to compare a local business's visibility across markets. The Standby example above uses country and language parameters; its current handler does not accept the batch interface's &lt;code&gt;location&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;These controls configure the search request, but they do not guarantee the exact results seen by a particular person. Personalization, timing, and source behavior can still differ. Keep the same settings when comparing rankings over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  What data export formats are supported?
&lt;/h3&gt;

&lt;p&gt;Batch results are stored in an Apify dataset, which you can export as JSON, CSV, Microsoft Excel (XLSX), XML, or an HTML table. Use JSON when you need nested AI citations, site links, or Knowledge Graph attributes; use a spreadsheet-friendly format for ranking reviews and content planning.&lt;/p&gt;

&lt;p&gt;You can also retrieve dataset items through the Apify API or Python client, then load them into your own database. Standby returns JSON directly to the caller. If you need a permanent history of those responses, save them in your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Scraping Google Search results for a 2027 workflow means looking beyond titles and links. Organic rankings, AI citations, local listings, and follow-up questions can answer different business questions, and a useful pipeline preserves both the data and the context in which it was collected.&lt;/p&gt;

&lt;p&gt;The three DIY approaches explain the trade-offs: an official API has its own availability and coverage limits, direct HTTP requests leave you responsible for parsing and incomplete pages, and headless browsers add an infrastructure and maintenance workload.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/google-search-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Google Search Scraper on the Apify platform&lt;/a&gt; packages supported search data into structured JSON, with batch runs for recurring research and a Standby endpoint for application requests. Start with a small query set, check which rich sections are returned, and use the live pricing to plan a workload you can sustain.&lt;/p&gt;

&lt;p&gt;Ready to turn search results into a useful dataset?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create your &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify account&lt;/a&gt; and review the current allowance.&lt;/li&gt;
&lt;li&gt;Open &lt;a href="https://apify.com/eunit/google-search-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Google Search Scraper in Apify Store&lt;/a&gt;, then check the available version, input fields, and &lt;strong&gt;Pricing&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Once you have access to the updated version, enter a few target queries, click &lt;strong&gt;Start&lt;/strong&gt;, and inspect the output before scheduling a larger run.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>google</category>
      <category>apify</category>
      <category>scraper</category>
      <category>search</category>
    </item>
    <item>
      <title>How to Build an Automated SEO Pipeline: Connecting Sitemap Generator and Google Indexer to Notion</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Wed, 09 Sep 2026 09:15:19 +0000</pubDate>
      <link>https://dev.to/apify/how-to-build-an-automated-seo-pipeline-connecting-sitemap-generator-and-google-indexer-to-notion-3f51</link>
      <guid>https://dev.to/apify/how-to-build-an-automated-seo-pipeline-connecting-sitemap-generator-and-google-indexer-to-notion-3f51</guid>
      <description>&lt;p&gt;Generating an XML sitemap is only the first step in modern technical Search Engine Optimization (SEO). If you have ever published a new blog post, launched a product line on an e-commerce platform, or updated critical landing pages, you know the frustration of waiting days, weeks, or even months for Google to crawl and index your content.&lt;/p&gt;

&lt;p&gt;Relying on search engine crawlers to discover changes organically is no longer viable for high-growth websites. If you run a dynamic content platform, an online store with rapidly changing inventory, or a news publication, delayed indexing directly equates to lost traffic, lower visibility, and missed revenue.&lt;/p&gt;

&lt;p&gt;In this guide, we will walk through the process of building an automated, enterprise-grade SEO pipeline. We will start by covering automated URL discovery using the &lt;a href="https://apify.com/eunit/sitemap-generator" rel="noopener noreferrer"&gt;Fast Sitemap Generator&lt;/a&gt; &lt;a href="https://www.apify.com?fpr=eunit" rel="noopener noreferrer"&gt;Apify Actor&lt;/a&gt;. Then, we will look at programmatically pushing those URLs to Google using the &lt;a href="https://apify.com/eunit/google-indexing" rel="noopener noreferrer"&gt;Google Indexer &amp;amp; Instant SEO Submitter&lt;/a&gt; Actor. Finally, we will solve the critical tracking challenge by using &lt;a href="https://blog.apify.com/what-is-model-context-protocol/" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; Connectors to stream real-time indexing logs directly into a &lt;a href="https://www.notion.so/" rel="noopener noreferrer"&gt;Notion&lt;/a&gt; database.&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%2F608psx0zx4lbunpittzs.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%2F608psx0zx4lbunpittzs.png" alt="Build an Automated SEO Pipeline: Connecting Sitemap Generator and Google Indexer to Notion" width="800" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The evolution of programmatic indexing
&lt;/h2&gt;

&lt;p&gt;Historically, website owners managed SEO by submitting a static Extensible Markup Language (XML) sitemap through &lt;a href="https://search.google.com/search-console" rel="noopener noreferrer"&gt;Google Search Console&lt;/a&gt;. While essential, submitting a sitemap is fundamentally a passive notification. You are effectively dropping a note in Google's mailbox, asking Googlebot to visit your site whenever it gets around to it.&lt;/p&gt;

&lt;p&gt;For small, static websites, this passive approach works. However, for modern dynamic sites, it falls short due to several structural challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Crawl budget constraints&lt;/strong&gt;: Search engines assign a finite "crawl budget" to every domain. On large sites with thousands of pages, Googlebot may exhaust its budget on legacy pages, completely ignoring newly published or updated content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orphaned or deep pages&lt;/strong&gt;: Content buried deep within subdirectories or pagination chains often fails to receive internal link equity, making it nearly invisible to organic crawling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-sensitive content&lt;/strong&gt;: E-commerce price updates, breaking news, real estate listings, and job postings require instant indexation. Waiting two weeks for organic discovery renders the content stale before it ever reaches search results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop crawler overhead&lt;/strong&gt;: Traditional site auditors like Screaming Frog run locally, consuming heavy system resources, tying up developer machines, and requiring manual CSV exports to process updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Moving from passive crawling to active push pipelines
&lt;/h3&gt;

&lt;p&gt;To solve these limitations, search engines introduced programmatic indexing APIs. Instead of waiting for Googlebot to discover changes, you can actively send a push notification to Google the instant a page is created or updated.&lt;/p&gt;

&lt;p&gt;By combining cloud-based crawling with active API submissions and real-time database logging, you can replace manual SEO workflows with a fully automated "set and forget" pipeline.&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%2Foea5w26jalag6dnu3dkk.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%2Foea5w26jalag6dnu3dkk.png" alt="Moving from passive crawling to active push pipelines" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1: Automated URL discovery with Fast Sitemap Generator
&lt;/h2&gt;

&lt;p&gt;Before you can submit URLs to Google, you need an accurate, deduplicated list of active links from your website. Hand-coding XML files or relying on CMS plugins that crash on large databases creates maintenance bottlenecks.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://apify.com/eunit/sitemap-generator" rel="noopener noreferrer"&gt;Fast Sitemap Generator&lt;/a&gt; Actor runs on the &lt;a href="https://apify.com" rel="noopener noreferrer"&gt;Apify platform&lt;/a&gt; as a serverless cloud program. It uses a Direct Connection to crawl websites at high speeds directly from data centers, without requiring proxy infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring the URL crawler
&lt;/h3&gt;

&lt;p&gt;Setting up automated URL discovery requires configuring key parameters inside the &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify Console&lt;/a&gt; to balance thoroughness with execution speed.&lt;/p&gt;

&lt;p&gt;Here is an example input configuration for a production crawl:&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;"startUrls"&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://example.com"&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;"maxCrawlDepth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPagesPerCrawl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includePatterns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;".*/blog/.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;".*/products/.*"&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;"excludePatterns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;".*/admin/.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;".*/login.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;".*/cart.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;".*/checkout/.*"&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;"sitemapFormats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"xml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"txt"&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;"respectRobotsTxt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changefreq"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"daily"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"defaultPriority"&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.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeImages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;h3&gt;
  
  
  Key configuration settings explained
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start URLs (&lt;code&gt;startUrls&lt;/code&gt;)&lt;/strong&gt;: The entry point for the crawler. Usually, this is your primary domain homepage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max Crawl Depth (&lt;code&gt;maxCrawlDepth&lt;/code&gt;)&lt;/strong&gt;: Controls how deep the crawler traverses link structures. A depth of &lt;code&gt;3&lt;/code&gt; to &lt;code&gt;5&lt;/code&gt; is typically sufficient to discover all essential content while preventing infinite loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regex Filtering (&lt;code&gt;includePatterns&lt;/code&gt; and &lt;code&gt;excludePatterns&lt;/code&gt;)&lt;/strong&gt;: Prevents cluttering your index with utility pages. You should explicitly exclude administrative paths like &lt;code&gt;.*/admin/.*&lt;/code&gt;, checkout pages &lt;code&gt;.*/cart.*&lt;/code&gt;, and query parameters that create duplicate content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Robots.txt Respect (&lt;code&gt;respectRobotsTxt&lt;/code&gt;)&lt;/strong&gt;: Ensures your pipeline adheres to site governance rules and crawler restrictions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Output storage and Dataset generation
&lt;/h3&gt;

&lt;p&gt;When execution completes, the Actor stores output artifacts in two locations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Key-Value Store&lt;/strong&gt;: Contains compiled &lt;code&gt;sitemap.xml&lt;/code&gt;, &lt;code&gt;sitemap.html&lt;/code&gt;, and &lt;code&gt;sitemap.txt&lt;/code&gt; files ready for static hosting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apify Dataset&lt;/strong&gt;: Stores structured JSON records for every discovered page. The Dataset ID produced during this run serves as the input payload for the next stage in our pipeline.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Stage 2: Programmatic submission via Google Cloud infrastructure
&lt;/h2&gt;

&lt;p&gt;With your site URLs discovered and structured into an Apify Dataset, the next phase is notifying Google. This step relies on the official Google Indexing API, which allows site owners to submit batch update requests directly to Google's indexing pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Setting up Google Cloud credentials
&lt;/h3&gt;

&lt;p&gt;Before making programmatic requests, you must configure authentication through the &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;Google Cloud Console&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.amazonaws.com%2Fuploads%2Farticles%2Fu4tc103vitcg242vl84s.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.amazonaws.com%2Fuploads%2Farticles%2Fu4tc103vitcg242vl84s.png" alt="Google Cloud Console" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a Google Cloud Project&lt;/strong&gt;: Log in to Google Cloud Console, click the project dropdown, and select &lt;strong&gt;New Project&lt;/strong&gt;. Name it &lt;code&gt;seo-automation-pipeline&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable the Indexing API&lt;/strong&gt;: Navigate to &lt;strong&gt;APIs &amp;amp; Services &amp;gt; Library&lt;/strong&gt;, search for "Web Search Indexing API", and click &lt;strong&gt;Enable&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a Service Account&lt;/strong&gt;: Go to &lt;strong&gt;IAM &amp;amp; Admin &amp;gt; Service Accounts&lt;/strong&gt;, click &lt;strong&gt;Create Service Account&lt;/strong&gt;, name it &lt;code&gt;indexing-bot&lt;/code&gt;, and assign it the &lt;strong&gt;Project &amp;gt; Editor&lt;/strong&gt; role.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate a JSON Key&lt;/strong&gt;: Select your new service account, go to the &lt;strong&gt;Keys&lt;/strong&gt; tab, click &lt;strong&gt;Add Key &amp;gt; Create new key&lt;/strong&gt;, select &lt;strong&gt;JSON&lt;/strong&gt;, and download the file. Keep this file secure, as it grants API permissions.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service Account Email Example:
indexing-bot@seo-automation-pipeline.iam.gserviceaccount.com
&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.amazonaws.com%2Fuploads%2Farticles%2Far9p2a94i4o4gi7wgley.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.amazonaws.com%2Fuploads%2Farticles%2Far9p2a94i4o4gi7wgley.png" alt="Enable Indexing API" width="800" height="396"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Granting Google Search Console access
&lt;/h3&gt;

&lt;p&gt;Enabling the API in Google Cloud is not enough on its own. Google requires verified domain ownership to prevent unauthorized indexing requests.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://search.google.com/search-console" rel="noopener noreferrer"&gt;Google Search Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Select your verified domain property.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Settings &amp;gt; Users and permissions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add user&lt;/strong&gt;, paste your service account email address, set the role to &lt;strong&gt;Owner&lt;/strong&gt;, and save.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;IMPORTANT&lt;br&gt;
Google Search Console requires &lt;strong&gt;Owner&lt;/strong&gt; permissions for Service Accounts calling the Indexing API. Setting lower permission levels such as "Full" or "Restricted" will cause authentication errors during API calls.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Bypassing DIY limitations: Python vs. Apify Actors
&lt;/h3&gt;

&lt;p&gt;Developers often attempt to write custom scripts to interact with the Google Indexing API. Below is a standard Python implementation using official Google authentication libraries:&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;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.oauth2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;service_account&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.auth.transport.requests&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AuthorizedSession&lt;/span&gt;

&lt;span class="n"&gt;SERVICE_ACCOUNT_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google-key.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;INDEXING_API_ENDPOINT&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://indexing.googleapis.com/v3/urlNotifications:publish&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;get_authenticated_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="n"&gt;scopes&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;https://www.googleapis.com/auth/indexing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="n"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_service_account_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
 &lt;span class="n"&gt;key_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;
 &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AuthorizedSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credentials&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;submit_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&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;action_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;URL_UPDATED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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;action_type&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INDEXING_API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="k"&gt;return&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="p"&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;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
 &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_authenticated_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SERVICE_ACCOUNT_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="n"&gt;target_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://example.com/blog/automated-seo-pipeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
 &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;submit_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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;Status Code: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&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="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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;Response: &lt;/span&gt;&lt;span class="si"&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;dumps&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="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="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;h3&gt;
  
  
  Why custom scripts break at scale
&lt;/h3&gt;

&lt;p&gt;While the DIY Python script works for single URLs, operating it in production exposes major infrastructure hurdles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit handling&lt;/strong&gt;: Google enforces daily quotas (typically 200 URL notifications per day for standard projects). Custom scripts will fail with &lt;code&gt;HTTP 429 Too Many Requests&lt;/code&gt; unless you write exponential backoff algorithms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State management&lt;/strong&gt;: A basic script does not remember which URLs were submitted yesterday. Running it daily resubmits duplicate URLs, wasting your daily quota.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution server maintenance&lt;/strong&gt;: You must deploy the script to a cloud server, manage cron schedules, and store credentials securely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The solution: Google Indexer &amp;amp; Instant SEO Submitter
&lt;/h3&gt;

&lt;p&gt;Instead of writing and hosting custom boilerplate code, we use the &lt;a href="https://apify.com/eunit/google-indexing" rel="noopener noreferrer"&gt;Google Indexer &amp;amp; Instant SEO Submitter&lt;/a&gt; Actor.&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%2Fo2soxiqbbn3b08op470c.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%2Fo2soxiqbbn3b08op470c.png" alt="Google Indexer &amp;amp; Instant SEO Submitter" width="800" height="788"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Custom Python Script&lt;/th&gt;
&lt;th&gt;Google Indexer Actor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quotas &amp;amp; Rate Limiting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual handling required&lt;/td&gt;
&lt;td&gt;Built-in automatic rate limiting &amp;amp; backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input Source&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hardcoded or manual files&lt;/td&gt;
&lt;td&gt;Direct integration with Apify Datasets &amp;amp; Sitemaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pricing Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server hosting costs&lt;/td&gt;
&lt;td&gt;Pay-Per-Event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Test Mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires code flags&lt;/td&gt;
&lt;td&gt;Built-in dry-run toggle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monitoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Custom logging setup&lt;/td&gt;
&lt;td&gt;Real-time console logs &amp;amp; dataset outputs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To configure the Actor, pass the JSON credentials and your Dataset ID from Stage 1 directly into the input schema:&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;"datasetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_STAGE_1_DATASET_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"serviceAccountJson"&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;"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;"service_account"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"project_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;"seo-automation-pipeline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"private_key_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;"abcdef123456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"private_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-----BEGIN PRIVATE KEY-----&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;-----END PRIVATE KEY-----&lt;/span&gt;&lt;span class="se"&gt;\n&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="nl"&gt;"client_email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"indexing-bot@seo-automation-pipeline.iam.gserviceaccount.com"&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;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"URL_UPDATED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"testMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;h3&gt;
  
  
  Stage 3: The missing link: Logging to Notion via MCP Connectors
&lt;/h3&gt;

&lt;p&gt;Automating crawls and API submissions solves the execution side of SEO. However, engineering teams and SEO leads still face a visibility gap: &lt;strong&gt;How do you monitor pipeline results without manually checking console logs after each run?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Exporting CSVs after every run is tedious, and setting up custom webhooks requires maintaining dedicated receiver endpoints. This is where &lt;a href="https://docs.apify.com/integrations/mcp-connectors" rel="noopener noreferrer"&gt;MCP connectors&lt;/a&gt; fit into the pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding MCP connectors
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://blog.apify.com/what-is-model-context-protocol/" rel="noopener noreferrer"&gt;The Model Context Protocol (MCP)&lt;/a&gt; is an open standard that lets Actors call third-party services like Notion, Slack, and GitHub on your behalf, using your own credentials, without the Actor ever holding your token directly.&lt;/p&gt;

&lt;p&gt;MCP connectors on Apify work differently from the Apify MCP server. The &lt;a href="https://mcp.apify.com" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt; exposes Apify Actors as tools to external AI clients (such as Claude or Cursor). MCP connectors do the opposite: they let Actors running on the Apify platform call external MCP-compatible services. The two are independent and can be used together.&lt;/p&gt;

&lt;p&gt;In this pipeline, we use the &lt;a href="https://apify.com/filip_cicvarek/notion-uploader" rel="noopener noreferrer"&gt;Notion Uploader&lt;/a&gt; Actor together with an Apify MCP connector to push the Google Indexer's results into a Notion database automatically after each run.&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%2Fxewket6m8suo4xdqm7yp.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%2Fxewket6m8suo4xdqm7yp.png" alt="Setting up the Notion database" width="800" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the Notion database
&lt;/h3&gt;

&lt;p&gt;Create a dedicated database in your Notion workspace titled &lt;strong&gt;SEO Indexing Tracker&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Configure the database columns with the following properties:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Column Name&lt;/th&gt;
&lt;th&gt;Property Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;URL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Title&lt;/td&gt;
&lt;td&gt;The target webpage URL submitted to Google&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Submission Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Select&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Indexed&lt;/code&gt;, &lt;code&gt;Failed&lt;/code&gt;, or &lt;code&gt;Quota Exceeded&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;The status response code returned by Google (e.g., &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;429&lt;/code&gt;, &lt;code&gt;403&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Last Updated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;Timestamp of when the request occurred&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source Actor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;td&gt;Name of the Apify Actor that executed the request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Authorizing the Notion MCP connector
&lt;/h3&gt;

&lt;p&gt;MCP connectors are authorized once at the account level, not on individual Actor or task pages. To set one up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify Console&lt;/a&gt; and go to &lt;strong&gt;Settings → API &amp;amp; Integrations → MCP connectors&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add connector&lt;/strong&gt; and select &lt;strong&gt;Notion&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Complete the OAuth flow — you will be redirected to Notion to authorize access to your workspace. The Apify platform never sees your token directly; authentication is handled server-side via the MCP proxy.&lt;/li&gt;
&lt;li&gt;Once authorized, the connector appears as a selectable option when running any compatible Actor.&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%2F7xr1hlpwn129dvuy2nlk.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%2F7xr1hlpwn129dvuy2nlk.png" alt="Notion MCP Connector" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting the Notion Uploader Actor
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://apify.com/filip_cicvarek/notion-uploader" rel="noopener noreferrer"&gt;Notion Uploader&lt;/a&gt; Actor reads any Apify dataset and uploads its rows into Notion. It accepts your MCP connector ID so you do not need to paste a Notion API key anywhere.&lt;/p&gt;

&lt;p&gt;When the Notion Uploader runs as an Actor-to-Actor integration (triggered by the completion of the Google Indexer run), it automatically picks up the triggering run's output dataset — no explicit &lt;code&gt;datasetId&lt;/code&gt; is needed in the payload template.&lt;/p&gt;

&lt;p&gt;Here is an example input for the Notion Uploader when configured as a standalone run:&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;"notionConnector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-connector-id&amp;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;"notionDatabaseId"&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.notion.so/workspace/SEO-Indexing-Tracker-&amp;lt;db-id&amp;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;"datasetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;google-indexer-output-dataset-id&amp;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;"dedupeMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upsert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyProperty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dryRun"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip&lt;/strong&gt;: Enable &lt;code&gt;"dryRun": true&lt;/code&gt; on your first run. It resolves the field mapping, validates it against your Notion schema, and shows exactly what would be written — without creating any rows.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Orchestrating the full "set and forget" pipeline
&lt;/h2&gt;

&lt;p&gt;Now that all three Actors are configured, we can chain them into an automated workflow: &lt;a href="https://apify.com/eunit/sitemap-generator" rel="noopener noreferrer"&gt;Fast Sitemap Generator&lt;/a&gt; discovers URLs, &lt;a href="https://apify.com/eunit/google-indexing" rel="noopener noreferrer"&gt;Google Indexer &amp;amp; Instant SEO Submitter&lt;/a&gt; submits them to Google, and &lt;a href="https://apify.com/filip_cicvarek/notion-uploader" rel="noopener noreferrer"&gt;Notion Uploader&lt;/a&gt; logs the results into Notion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Creating saved Actor tasks
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;Actor task&lt;/strong&gt; is a saved, reusable configuration of an Actor in &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify Console&lt;/a&gt;. Tasks let you pre-define all input parameters so they can be run on demand, via API, or on a schedule.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Crawler task&lt;/strong&gt;: Open &lt;a href="https://apify.com/eunit/sitemap-generator" rel="noopener noreferrer"&gt;Fast Sitemap Generator&lt;/a&gt; in Apify Console, configure your &lt;code&gt;startUrls&lt;/code&gt;, crawl depth, and regex filters, then click &lt;strong&gt;Save as a new task&lt;/strong&gt; in the top-right corner. Name it &lt;code&gt;sitemap-generator-task&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indexer task&lt;/strong&gt;: Open &lt;a href="https://apify.com/eunit/google-indexing" rel="noopener noreferrer"&gt;Google Indexer &amp;amp; Instant SEO Submitter&lt;/a&gt;, fill in your Google Cloud Service Account JSON key, then click &lt;strong&gt;Save as a new task&lt;/strong&gt;. Name it &lt;code&gt;google-indexer-task&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uploader task&lt;/strong&gt;: Open &lt;a href="https://apify.com/filip_cicvarek/notion-uploader" rel="noopener noreferrer"&gt;Notion Uploader&lt;/a&gt;, set your Notion MCP connector and target database URL, then click &lt;strong&gt;Save as a new task&lt;/strong&gt;. Name it &lt;code&gt;notion-uploader-task&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All saved tasks are listed under &lt;strong&gt;Saved tasks&lt;/strong&gt; in the Apify Console left navigation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Chaining tasks using Actor-to-Actor integrations
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Integrations&lt;/strong&gt; tab on each Actor or task page lets you chain runs together. When one task succeeds, Apify automatically triggers the next one and passes dynamic run data — such as the output dataset ID — via a payload template.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chain 1: Sitemap Generator → Google Indexer
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;sitemap-generator-task&lt;/code&gt; in Apify Console and click the &lt;strong&gt;Integrations&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add integration&lt;/strong&gt; and select &lt;strong&gt;Run another Actor or task&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;code&gt;google-indexer-task&lt;/code&gt; as the target.&lt;/li&gt;
&lt;li&gt;Set the trigger to &lt;strong&gt;Run succeeded&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Set the payload template so the indexer receives the crawler's output dataset:
&lt;/li&gt;
&lt;/ol&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;"datasetId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{resource.defaultDatasetId}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"URL_UPDATED"&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;h3&gt;
  
  
  Chain 2: Google Indexer → Notion Uploader
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;google-indexer-task&lt;/code&gt; in Apify Console and click the &lt;strong&gt;Integrations&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add integration&lt;/strong&gt; and select &lt;strong&gt;Run another Actor or task&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;code&gt;notion-uploader-task&lt;/code&gt; as the target.&lt;/li&gt;
&lt;li&gt;Set the trigger to &lt;strong&gt;Run succeeded&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Leave &lt;code&gt;datasetId&lt;/code&gt; out of the payload template — the Notion Uploader automatically uses the triggering run's dataset when no explicit ID is provided:
&lt;/li&gt;
&lt;/ol&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;"dedupeMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upsert"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyProperty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"URL"&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;Now the full chain fires end-to-end on its own. When &lt;code&gt;sitemap-generator-task&lt;/code&gt; completes, Apify triggers &lt;code&gt;google-indexer-task&lt;/code&gt; with the crawler's dataset. When the indexer finishes, Apify triggers &lt;code&gt;notion-uploader-task&lt;/code&gt;, which reads the indexer's output dataset and writes each result row into your Notion database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Scheduling automated pipeline execution
&lt;/h3&gt;

&lt;p&gt;Configure a recurring schedule so the entire chain runs automatically.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the Apify Console left sidebar, click &lt;strong&gt;Schedules&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create new schedule&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;sitemap-generator-task&lt;/code&gt; as the target. The full chain — crawl, index, upload to Notion — fires automatically when this task runs.&lt;/li&gt;
&lt;li&gt;Set the frequency using a Cron expression or one of the presets.&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For high-frequency publishing (news, job boards)&lt;/strong&gt;: Schedule daily at 2:00 AM UTC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For standard marketing blogs and commercial sites&lt;/strong&gt;: Schedule weekly, every Monday morning.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cron expression example (daily at 2:00 AM UTC):
0 2 * * *
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Best practices for automated SEO indexing
&lt;/h2&gt;

&lt;p&gt;To maintain a healthy indexing strategy while avoiding Google API flags, keep these best practices in mind:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Submit only modified or new content
&lt;/h3&gt;

&lt;p&gt;Do not resubmit your entire site inventory every day. The Google Indexing API is intended for pages that have new content (&lt;code&gt;URL_UPDATED&lt;/code&gt;) or pages that have been deleted (&lt;code&gt;URL_DELETED&lt;/code&gt;). Resubmitting unchanged pages wastes daily quotas and can result in rate limiting.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Match canonical URLs strictly
&lt;/h3&gt;

&lt;p&gt;Google requires strict URL matching. If your canonical structure uses &lt;code&gt;https://example.com/blog/post-name/&lt;/code&gt; with a trailing slash, ensure your crawler filters and API submissions match that format exactly. Submitting &lt;code&gt;http://&lt;/code&gt; or non-trailing-slash variations can trigger redirect loops and indexing failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use Test Mode during initial setup
&lt;/h3&gt;

&lt;p&gt;Before running full-scale API operations, turn on the &lt;strong&gt;Test Mode&lt;/strong&gt; toggle in the &lt;a href="https://apify.com/eunit/google-indexing" rel="noopener noreferrer"&gt;Google Indexer &amp;amp; Instant SEO Submitter&lt;/a&gt; Actor. Test Mode executes the full crawl, verifies JSON authentication keys, and simulates Notion MCP logging without consuming your daily Google API quota.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Monitor quota usage in Google Cloud Console
&lt;/h3&gt;

&lt;p&gt;Standard Google Cloud projects receive an initial quota of 200 Indexing API requests per day. If your site publishes hundreds of new pages daily, submit a quota increase request directly through the &lt;strong&gt;Quotas&lt;/strong&gt; tab in Google Cloud Console.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Building an automated technical SEO pipeline transforms indexing from a passive waiting game into a predictable, programmatic system.&lt;/p&gt;

&lt;p&gt;Combining the cloud crawling capabilities of the &lt;a href="https://apify.com/eunit/sitemap-generator" rel="noopener noreferrer"&gt;Fast Sitemap Generator&lt;/a&gt;, the push notifications of the &lt;a href="https://apify.com/eunit/google-indexing" rel="noopener noreferrer"&gt;Google Indexer &amp;amp; Instant SEO Submitter&lt;/a&gt;, and real-time database logging through &lt;a href="https://apify.com/store/collections/mcp-connectors" rel="noopener noreferrer"&gt;Notion MCP Connectors&lt;/a&gt;, you establish an automated SEO engine that operates reliably in the background.&lt;/p&gt;

&lt;p&gt;Instead of waiting weeks for Google to discover your newest content organically, your site updates are crawled, verified, submitted, and logged in Notion within minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read more
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Explore the &lt;a href="https://apify.com/eunit/sitemap-generator" rel="noopener noreferrer"&gt;Fast Sitemap Generator&lt;/a&gt; on the Apify Store.&lt;/li&gt;
&lt;li&gt;Set up the &lt;a href="https://apify.com/eunit/google-indexing" rel="noopener noreferrer"&gt;Google Indexer &amp;amp; Instant SEO Submitter&lt;/a&gt; Actor with your Google Cloud Service Account.&lt;/li&gt;
&lt;li&gt;Connect your Notion workspace via MCP Connectors to start tracking real-time indexing status today.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>seoautomation</category>
      <category>googleindexingapi</category>
      <category>technicalseo</category>
      <category>webcrawling</category>
    </item>
    <item>
      <title>How to Automate Instagram Reels to Notion via MCP</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Wed, 09 Sep 2026 09:14:53 +0000</pubDate>
      <link>https://dev.to/apify/how-to-automate-instagram-reels-to-notion-via-mcp-b55</link>
      <guid>https://dev.to/apify/how-to-automate-instagram-reels-to-notion-via-mcp-b55</guid>
      <description>&lt;p&gt;Managing short-form video content across multiple campaigns often turns into an administrative nightmare. Your marketing team publishes dozens of public Reels, short video posts, and IGTV clips every month. Everything looks great on Instagram, but as soon as someone asks to archive these assets into a centralized brand library or content calendar, the entire operation grinds to a halt.&lt;/p&gt;

&lt;p&gt;Suddenly, you are forced to rely on shady, ad-heavy download websites that fail half the time, paste individual links into manual file converters, or copy caption text into spreadsheets by hand. Even when you do manage to extract a direct video file URL from Instagram, that URL expires within hours due to Instagram's content delivery network (CDN) session restrictions.&lt;/p&gt;

&lt;p&gt;What if you could &lt;a href="https://www.eunit.me/blog/how-to-download-instagram-videos-and-reels-at-scale-with-apify" rel="noopener noreferrer"&gt;automatically extract high-quality Instagram video files&lt;/a&gt;, store them permanently, and stream that structured data straight into Notion in real time?&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt;, the &lt;a href="https://apify.com/filip_cicvarek/notion-uploader" rel="noopener noreferrer"&gt;Notion Uploader&lt;/a&gt; Actor, and Apify &lt;a href="https://mcp.apify.com/?fpr=eunit" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; connectors, you can build a zero-maintenance, automated pipeline. This guide walks you through setting up an automated Instagram-to-Notion video content calendar that automatically syncs dataset outputs upon run completion without requiring manual CSV exports or fragile custom backend glue code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://postimg.cc/9z9rTp65" rel="noopener noreferrer"&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%2F3ctibhb1sweb6m14nu7s.png" alt="How to Automate Instagram Reels to Notion via MCP" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The content archiving nightmare
&lt;/h2&gt;

&lt;p&gt;Picture this scenario. You are a creative organization, and your creative team just finished a quarter-long influencer campaign. Thirty public Instagram Reels and video posts were published. Your client or department head wants every asset logged inside Notion with direct video links, preview thumbnails, shortcodes, captions, and publishing dates for future repurposing.&lt;/p&gt;

&lt;p&gt;The traditional approach to this task usually follows one of two painful paths:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The manual downloader route:&lt;/strong&gt; A team member spends hours copying Instagram links, pasting them into third-party downloader sites, closing popup ads, downloading &lt;code&gt;.mp4&lt;/code&gt; files locally, uploading those files into Google Drive or Notion, and typing metadata by hand. This process consumes dozens of work hours, produces inconsistent video resolutions, and risks downloading malware.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The legacy scraping route:&lt;/strong&gt; A developer sets up a web scraper to pull Instagram data. The scraper runs for 10 minutes, completes its execution, and dumps a static JSON or CSV file to cloud storage. A developer then has to write custom webhooks or cron scripts to parse that output file, format the payload, and send REST API requests to Notion. If any record fails or if the script breaks mid-transit, debugging becomes a headache.
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["Scraper / Web Downloader"] --&amp;gt; B["CSV / JSON Export"]
    B --&amp;gt; C["Custom Glue Script / Manual Upload"]
    C --&amp;gt; D["Notion Database"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Neither approach is ideal for growing teams or production workflows. Legacy scraping forces you to write custom integration pipelines to move data downstream. Meanwhile, manual downloading fails to scale beyond a handful of links.&lt;/p&gt;

&lt;p&gt;Apify Actor integrations and &lt;a href="https://mcp.apify.com/?fpr=eunit" rel="noopener noreferrer"&gt;Apify MCP&lt;/a&gt; connectors solve this problem seamlessly. Rather than manually writing custom export scripts or handling fragile API glue, you can attach the supported &lt;a href="https://apify.com/filip_cicvarek/notion-uploader?fpr=eunit" rel="noopener noreferrer"&gt;Notion Uploader&lt;/a&gt; Actor directly to your extraction workflow inside Apify Console.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["Instagram Reels"] --&amp;gt; B["Instagram Video and Reel Downloader"]
    B -- On Run Succeeded --&amp;gt; C["Notion Uploader Actor Integration"]
    C -- Apify MCP Connector / API Key --&amp;gt; D["Notion Content Calendar"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Once &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader?fpr=eunit" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; finishes extracting video assets and storing permanent &lt;code&gt;.mp4&lt;/code&gt; file links in Apify Key-Value Store, Apify automatically executes the &lt;strong&gt;Notion Uploader&lt;/strong&gt; integration. &lt;strong&gt;Notion Uploader&lt;/strong&gt; connects securely to your Notion workspace using an Apify MCP connector (configured in Apify Console under &lt;strong&gt;Settings → Integrations → MCP connectors&lt;/strong&gt;) or a Notion API key, mapping fields and creating structured database rows automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline architecture: how MCP connectors change the game
&lt;/h2&gt;

&lt;p&gt;To understand why this architecture is so powerful, let's examine what happens under the hood during an automated run.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph APIFY["APIFY PLATFORM"]
        A["Instagram /reel/ URLs"] --&amp;gt; B["Dual-Path Extraction Engine"]
        B --&amp;gt; C["Apify Key-Value Store&amp;lt;br/&amp;gt;(Permanent Video URL)"]
        B --&amp;gt; D["Apify Dataset&amp;lt;br/&amp;gt;(Structured Metadata)"]
    end

    D -- On Run Succeeded --&amp;gt; E["Notion Uploader Actor&amp;lt;br/&amp;gt;(filip_cicvarek/notion-uploader)"]
    E -- Apify MCP Connector / API Key --&amp;gt; F["Notion Content Calendar&amp;lt;br/&amp;gt;(Automated Database Rows)"]&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  1. Dual-path extraction engine
&lt;/h3&gt;

&lt;p&gt;Instagram does not provide direct, permanent video download links through a simple public REST API. Furthermore, different post types behave differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some standard post pages expose media tags within initial static HTML.&lt;/li&gt;
&lt;li&gt;Complex Reel pages render content dynamically via JavaScript, obscuring video source URLs behind client-side network calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; handles this seamlessly using a dual-path architecture. It first attempts a lightweight static HTTP request to extract the media link instantly. If the target page requires dynamic execution, the Actor automatically falls back to a browser session to capture media delivery requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Solving CDN URL expiration via Key-Value Store
&lt;/h3&gt;

&lt;p&gt;Direct video URLs returned straight from Instagram's servers (&lt;code&gt;*.cdninstagram.com&lt;/code&gt; or &lt;code&gt;*.fbcdn.net&lt;/code&gt;) are session-bound and expire after a short period. If you paste a raw CDN video link into Notion, the video will stop playing within 24 to 48 hours.&lt;/p&gt;

&lt;p&gt;To solve this, &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; includes a &lt;code&gt;storeVideoFiles&lt;/code&gt; configuration option in Apify Console. When set to &lt;code&gt;true&lt;/code&gt;, the Actor streams the raw &lt;code&gt;.mp4&lt;/code&gt; file directly into Apify Key-Value Store during the run. This generates a permanent, publicly accessible URL (&lt;code&gt;videoPublicUrl&lt;/code&gt;) that Notion can embed reliably forever.&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%2Ftkdqxri0e2gz5lsg4i1z.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%2Ftkdqxri0e2gz5lsg4i1z.png" alt="Instagram Video and Reel Downloader Input on Apify Console" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Automated dataset push via Notion Uploader and MCP connectors
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/docs/2026-07-28/getting-started/intro" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; is an open standard that allows tools and AI systems to exchange context and perform actions securely across application boundaries. In the Apify ecosystem, MCP connectors allow Actors like &lt;a href="https://apify.com/filip_cicvarek/notion-uploader" rel="noopener noreferrer"&gt;Notion Uploader&lt;/a&gt; to communicate directly with external services - such as Notion, HubSpot, Google Sheets, or Slack.&lt;/p&gt;

&lt;p&gt;By configuring an &lt;strong&gt;Actor Integration&lt;/strong&gt; on &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; set to trigger on &lt;code&gt;Run succeeded&lt;/code&gt;, Apify automatically hands off the generated dataset to &lt;strong&gt;Notion Uploader&lt;/strong&gt;. Notion Uploader reads the structured items and uses the configured Notion MCP connector (or Notion API key) to map and upsert rows directly into your Notion database.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Zero-trust token security and governance
&lt;/h3&gt;

&lt;p&gt;When connecting third-party services to web automations, storing API tokens in raw scripts or public Actor inputs poses severe security risks. Apify handles MCP authentication via OAuth 2.0 proxies managed centrally inside Apify Console under &lt;strong&gt;Settings → Integrations → MCP connectors&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your Notion authorization token is never exposed in plain text, never saved in Actor dataset output, and never shared with the underlying script environment. The Apify platform handles identity verification and permission scope tokens securely behind the scenes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Setting up the Notion database
&lt;/h2&gt;

&lt;p&gt;Before configuring the Actor, you need a designated Notion database structured to store video metadata and file references.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your Notion workspace and create a new database page (select &lt;strong&gt;Full page database&lt;/strong&gt; or &lt;strong&gt;Inline database&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Name your database &lt;code&gt;Instagram Content Calendar&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Configure the database properties to match the schema exported by &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property Name&lt;/th&gt;
&lt;th&gt;Notion Property Type&lt;/th&gt;
&lt;th&gt;Purpose / Payload Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Title&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Title&lt;/td&gt;
&lt;td&gt;Stores post caption snippet or title (&lt;code&gt;title&lt;/code&gt; / &lt;code&gt;description&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instagram Link&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;Direct link to original Instagram post (&lt;code&gt;canonicalUrl&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Direct Video Link&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;URL&lt;/td&gt;
&lt;td&gt;Permanent Apify storage link (&lt;code&gt;videoPublicUrl&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Thumbnail&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Files &amp;amp; media (or URL)&lt;/td&gt;
&lt;td&gt;Preview thumbnail image (&lt;code&gt;thumbnailUrl&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shortcode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text&lt;/td&gt;
&lt;td&gt;Instagram unique ID (&lt;code&gt;shortcode&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Media Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Select&lt;/td&gt;
&lt;td&gt;Post type (&lt;code&gt;reel&lt;/code&gt;, &lt;code&gt;post&lt;/code&gt;, or &lt;code&gt;tv&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fetch Date&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Date&lt;/td&gt;
&lt;td&gt;Timestamp of processing (&lt;code&gt;fetchedAt&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Status&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Status / Select&lt;/td&gt;
&lt;td&gt;Processing result (&lt;code&gt;ready&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;erDiagram
    INSTAGRAM_CONTENT_CALENDAR {
        string Title "Summer Campaign Teaser"
        string Instagram_Link "instagram.com/reel/C123abc/"
        string Direct_Video_Link "api.apify.com/.../video-c123.mp4"
        string Media_Type "reel"
        string Status "ready"
    }&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Ensure your Notion database view is set up to display URL and media properties clearly so your creative team can preview videos directly within the calendar workspace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Configuring Instagram Video and Reel Downloader
&lt;/h2&gt;

&lt;p&gt;With your Notion database ready, navigate to &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; on Apify Store.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Try for free&lt;/strong&gt; or open the Actor inside Apify Console. Select the &lt;strong&gt;Input&lt;/strong&gt; tab to configure your run.&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%2Fy28uyvs58qmcofmtgmru.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%2Fy28uyvs58qmcofmtgmru.png" alt="Instagram Video Downloader Actor" width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Input JSON configuration
&lt;/h3&gt;

&lt;p&gt;To ensure video URLs remain active inside Notion indefinitely, you must set &lt;code&gt;storeVideoFiles&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;. This instructs the Actor to download the binary media file into the Apify Key-Value Store and produce the &lt;code&gt;videoPublicUrl&lt;/code&gt; parameter.&lt;/p&gt;

&lt;p&gt;Here is an ideal production configuration snippet:&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;"startUrls"&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://www.instagram.com/reel/C8XyZ123abc/"&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://www.instagram.com/p/C9AbC456def/"&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://www.instagram.com/reel/C-Ghi789jkl/"&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;"storeVideoFiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"storeThumbnailFiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includePageHtml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filenamePrefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notion-archive-2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&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;"maxRequestRetries"&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;"requestTimeoutSecs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proxyConfiguration"&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;"useApifyProxy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="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;h3&gt;
  
  
  Key configuration settings explained
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;startUrls&lt;/code&gt;&lt;/strong&gt;: An array of target public Instagram URLs. Supports &lt;code&gt;/reel/&lt;/code&gt;, &lt;code&gt;/p/&lt;/code&gt;, and &lt;code&gt;/tv/&lt;/code&gt; paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;storeVideoFiles&lt;/code&gt;&lt;/strong&gt;: Set to &lt;code&gt;true&lt;/code&gt;. This generates a permanent &lt;code&gt;videoPublicUrl&lt;/code&gt; host link on &lt;code&gt;api.apify.com&lt;/code&gt;, avoiding expired CDN link errors inside Notion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;storeThumbnailFiles&lt;/code&gt;&lt;/strong&gt;: Set to &lt;code&gt;true&lt;/code&gt; if you want stored key-value records for thumbnail images as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;filenamePrefix&lt;/code&gt;&lt;/strong&gt;: A custom string prepended to all stored files inside Apify storage (e.g., &lt;code&gt;notion-archive-2026&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;maxConcurrency&lt;/code&gt;&lt;/strong&gt;: Set to &lt;code&gt;2&lt;/code&gt; or &lt;code&gt;3&lt;/code&gt;. Instagram actively monitors aggressive parallel scraping. Keeping concurrency low ensures reliable extraction without triggering rate limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;proxyConfiguration&lt;/code&gt;&lt;/strong&gt;: Enabling Apify Proxy distributes requests across residential or datacenter IP pools, preventing unexpected block screens.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3: Wiring the Notion Uploader integration in Apify Console
&lt;/h2&gt;

&lt;p&gt;Once your input JSON is defined for &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt;, you attach the &lt;a href="https://apify.com/filip_cicvarek/notion-uploader" rel="noopener noreferrer"&gt;Notion Uploader&lt;/a&gt; Actor integration directly to the run configuration inside Apify Console.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Creating your Notion MCP connector (or API key)
&lt;/h3&gt;

&lt;p&gt;Before attaching the integration, establish your connection to Notion inside Apify Console:&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%2Fnn8my7i1i43ruc2znp0p.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%2Fnn8my7i1i43ruc2znp0p.png" alt="Notion MCP Connector" width="800" height="559"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Apify Console, open &lt;strong&gt;Settings → API &amp;amp; Integrations → MCP connectors&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add connector&lt;/strong&gt; and select &lt;strong&gt;Notion&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Complete the one-click OAuth authorization window to grant Apify access to your Notion workspace and database.&lt;/li&gt;
&lt;li&gt;Apify Console generates a secure connector ID (e.g. &lt;code&gt;mcp-connector-...&lt;/code&gt;) that allows Actors to interact with Notion with your permissions, without ever exposing raw private tokens.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;(Alternative: You can also use a classic internal integration secret starting with &lt;code&gt;ntn_&lt;/code&gt; created at &lt;a href="https://www.notion.so/profile/integrations" rel="noopener noreferrer"&gt;notion.so/profile/integrations&lt;/a&gt;. Be sure to invite your integration to the Notion database via *&lt;/em&gt;••• → Connections*&lt;em&gt;).&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    A["Apify Console Settings"] --&amp;gt; B["MCP Connectors"]
    B --&amp;gt; C["Add Notion Connector (OAuth)"]
    C --&amp;gt; D["Secure Connector Session ID"]&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  2. Adding the Notion Uploader integration to your Actor
&lt;/h3&gt;

&lt;p&gt;With your Notion MCP connector ready, wire the integration to &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&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%2Fh1en78eephqqy4ci6l1j.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%2Fh1en78eephqqy4ci6l1j.png" alt="Instagram Video Downloader + Notion Uploader" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; in Apify Console.&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;Integrations&lt;/strong&gt; tab located in the navigation panel below the Actor title.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add integration&lt;/strong&gt;. Under &lt;strong&gt;Suggested for this Actor&lt;/strong&gt;, click &lt;strong&gt;Notion Uploader&lt;/strong&gt; (&lt;code&gt;filip_cicvarek/notion-uploader&lt;/code&gt;). &lt;em&gt;(Note: Apify Console also lists third-party integration routes like Make and Zapier under "Integrate with third-party tools", but Notion Uploader provides the direct, native Apify Actor integration path)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Start when&lt;/strong&gt; to &lt;strong&gt;Run succeeded&lt;/strong&gt;. This ensures Notion Uploader fires automatically the moment your Instagram extraction run completes cleanly.&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%2F7ym66zaa9rrtiaitplrq.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%2F7ym66zaa9rrtiaitplrq.png" alt="Instagram Video Downloader + Notion Uploader" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Mapping fields and configuring Notion Uploader input
&lt;/h3&gt;

&lt;p&gt;Under the &lt;strong&gt;Actor input&lt;/strong&gt; section for the Notion Uploader integration, configure the target database and field mapping:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Notion connector&lt;/strong&gt;: Select your authorized Notion MCP connector from the dropdown list (or paste your &lt;code&gt;notionApiKey&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notion database&lt;/strong&gt;: Paste your Notion database URL or ID (&lt;code&gt;notionDatabaseId&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit field mapping (&lt;code&gt;dataMapping&lt;/code&gt;)&lt;/strong&gt;: Map the dataset output fields from Instagram Video and Reel Downloader to your Notion database properties:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Instagram 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;"canonicalUrl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Direct Video 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;"videoPublicUrl"&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;"thumbnailUrl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Shortcode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shortcode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Media 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;"mediaType"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Fetch Date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fetchedAt"&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate handling (rerun safety)&lt;/strong&gt;: Set &lt;strong&gt;Dedupe mode&lt;/strong&gt; (&lt;code&gt;dedupeMode&lt;/code&gt;) to &lt;code&gt;upsert&lt;/code&gt; and set &lt;strong&gt;Key property&lt;/strong&gt; (&lt;code&gt;keyProperty&lt;/code&gt;) to &lt;code&gt;Instagram Link&lt;/code&gt; (or &lt;code&gt;Shortcode&lt;/code&gt;). This guarantees that re-running the Actor with previously processed URLs will update existing database rows instead of creating duplicate entries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Click &lt;strong&gt;Save integration&lt;/strong&gt;. The integration is now active and bound to your Actor task.&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%2Fcw3ftbv193j5mqedcmt1.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%2Fcw3ftbv193j5mqedcmt1.gif" alt="Instagram Video and Reel Downloader gif" width="560" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Executing the run and verifying the content calendar
&lt;/h2&gt;

&lt;p&gt;With the input parameters set and the Notion Uploader integration saved, click the &lt;strong&gt;Start&lt;/strong&gt; button in the top-right corner of Apify Console.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watching automated workflow execution
&lt;/h3&gt;

&lt;p&gt;As &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; runs, you can monitor the progress through the &lt;strong&gt;Log&lt;/strong&gt; tab:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Actor extracts public Instagram video posts, Reels, and IGTV links specified in &lt;code&gt;startUrls&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For each URL, it resolves media endpoints and streams binary &lt;code&gt;.mp4&lt;/code&gt; files into Apify Key-Value Store, generating permanent &lt;code&gt;videoPublicUrl&lt;/code&gt; host links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration handoff&lt;/strong&gt;: When the extraction run finishes successfully (&lt;code&gt;Run succeeded&lt;/code&gt;), Apify immediately launches &lt;strong&gt;Notion Uploader&lt;/strong&gt; (&lt;code&gt;filip_cicvarek/notion-uploader&lt;/code&gt;), passing the run's default dataset ID automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notion Database update&lt;/strong&gt;: Notion Uploader uses your Notion MCP connector to read the dataset items, validate properties against your database schema, apply &lt;code&gt;upsert&lt;/code&gt; deduplication, and populate your &lt;code&gt;Instagram Content Calendar&lt;/code&gt; in Notion.
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant Downloader as Instagram Video Downloader
    participant KVS as Key-Value Store
    participant Integration as Notion Uploader Integration
    participant Notion as Notion Database

    Note over Downloader: Downloader starts execution
    Downloader-&amp;gt;&amp;gt;KVS: Extracts Reel &amp;amp; uploads .mp4 file
    Note over Downloader: Run completes (Status: SUCCESS)
    Downloader-&amp;gt;&amp;gt;Integration: Triggers Notion Uploader automatically
    Integration-&amp;gt;&amp;gt;Notion: Reads dataset &amp;amp; upserts database rows
    Note over Notion: Content calendar fully updated!&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If an invalid or private URL is encountered, the extraction Actor records a structured dataset item containing an &lt;code&gt;errorCode&lt;/code&gt; (such as &lt;code&gt;NO_VIDEO_URL&lt;/code&gt;) and &lt;code&gt;errorMessage&lt;/code&gt;. Notion Uploader writes an &lt;code&gt;error&lt;/code&gt; status row into Notion, maintaining a complete audit log without breaking the pipeline execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Extending the workflow with API triggers and scheduling
&lt;/h2&gt;

&lt;p&gt;Building a one-off run inside Apify Console is great for testing, but real content operations require automated execution. You can automate this pipeline using code triggers or scheduled jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Programmatic execution via JavaScript / TypeScript
&lt;/h3&gt;

&lt;p&gt;You can trigger the integrated pipeline programmatically using the official &lt;code&gt;apify-client&lt;/code&gt; SDK in Node.js or TypeScript by invoking your saved task (which includes the Notion Uploader integration):&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;ApifyClient&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;apify-client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Initialize the client with your Apify API token&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&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;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_APIFY_API_TOKEN&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;// Define input parameters with video storage enabled&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;startUrls&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="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="s1"&gt;https://www.instagram.com/reel/C8XyZ123abc/&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="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="s1"&gt;https://www.instagram.com/reel/C9AbC456def/&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="na"&gt;storeVideoFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;storeThumbnailFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;filenamePrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;weekly-content-sync&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxConcurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;proxyConfiguration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;useApifyProxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;runInstagramSync&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="s1"&gt;Starting Instagram to Notion sync workflow...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Call the saved task (or Actor). Configured integrations run automatically on completion.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;run&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_SAVED_TASK_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;`Run completed! View dataset at: https://console.apify.com/storage/datasets/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultDatasetId&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Notion Uploader integration triggered automatically.&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;runInstagramSync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Programmatic execution via Python
&lt;/h3&gt;

&lt;p&gt;If your infrastructure runs on Python, use &lt;code&gt;apify-client&lt;/code&gt; to trigger the sync:&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;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize client
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_APIFY_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Prepare input dictionary
&lt;/span&gt;&lt;span class="n"&gt;run_input&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;startUrls&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.instagram.com/reel/C8XyZ123abc/&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.instagram.com/reel/C9AbC456def/&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;storeVideoFiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;storeThumbnailFiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filenamePrefix&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;python-content-sync&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;maxConcurrency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxyConfiguration&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;useApifyProxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Run the saved task with Notion integration attached
&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;Launching Instagram Video Downloader with Notion Uploader Integration...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run&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;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_SAVED_TASK_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_input&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;Workflow complete. Dataset ID: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&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;h3&gt;
  
  
  Automated set-and-forget scheduling
&lt;/h3&gt;

&lt;p&gt;To eliminate manual execution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Save your input configuration and Notion Uploader integration inside Apify Console as a &lt;strong&gt;Saved Task&lt;/strong&gt; (e.g., &lt;code&gt;Instagram-to-Notion-Sync-Task&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Navigate to the &lt;strong&gt;Schedules&lt;/strong&gt; tab in Apify Console and click &lt;strong&gt;Create schedule&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Link the schedule to your saved Actor task.&lt;/li&gt;
&lt;li&gt;Set a cron cadence, such as every Friday at 5:00 PM (&lt;code&gt;0 17 * * 5&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every week, Apify will automatically run &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt;, extract new video links, upload permanent &lt;code&gt;.mp4&lt;/code&gt; binaries to Key-Value Store, and trigger &lt;strong&gt;Notion Uploader&lt;/strong&gt; to push the assets into your Notion database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Relying on ad-ridden video download websites, manual file management, and fragile spreadsheet uploads is no longer necessary for modern media operations.&lt;/p&gt;

&lt;p&gt;By combining the extraction capabilities of &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; with the &lt;a href="https://apify.com/filip_cicvarek/notion-uploader" rel="noopener noreferrer"&gt;Notion Uploader&lt;/a&gt; Actor and Apify MCP connectors, you can build a seamless content archiving pipeline. Permanent video hosting via Apify Key-Value Store eliminates broken CDN links, while automated Actor integrations push structured data straight into Notion without requiring custom backend glue code.&lt;/p&gt;

&lt;p&gt;Ready to automate your social media asset pipeline?&lt;/p&gt;

&lt;p&gt;Try &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader on Apify Store&lt;/a&gt; today, connect your Notion workspace via Notion Uploader, and build your automated content calendar in minutes.&lt;/p&gt;

</description>
      <category>notion</category>
      <category>apify</category>
      <category>webscraping</category>
      <category>instagramreels</category>
    </item>
    <item>
      <title>Headless browsers vs. SERP APIs: Why your Playwright scraper fails in production</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Wed, 09 Sep 2026 09:14:32 +0000</pubDate>
      <link>https://dev.to/eunit/headless-browsers-vs-serp-apis-why-your-playwright-scraper-fails-in-production-4cgo</link>
      <guid>https://dev.to/eunit/headless-browsers-vs-serp-apis-why-your-playwright-scraper-fails-in-production-4cgo</guid>
      <description>&lt;p&gt;Every developer who builds web scrapers goes through the same initial rite of passage. You need search engine data for an SEO tracking tool, an AI retrieval pipeline, or a market research project. Your first instinct is to reach for a browser automation tool like &lt;a href="https://www.eunit.me/blog/playwright-vs-webdriverio-uncovering-the-differences" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt;, Puppeteer, or Selenium.&lt;/p&gt;

&lt;p&gt;You write a quick script, launch a &lt;a href="https://www.browserless.io/blog/headless-chrome" rel="noopener noreferrer"&gt;headless Chromium instance&lt;/a&gt;, navigate to Google, inspect the Document Object Model (DOM), and extract text from &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; tags and organic result containers.&lt;/p&gt;

&lt;p&gt;On your local development machine, it feels like magic. The script runs cleanly for your first five test queries.&lt;/p&gt;

&lt;p&gt;Then, you deploy your script to a production server on AWS or DigitalOcean, and everything breaks down. Within minutes, your scraper encounters roadblock CAPTCHAs, gets flagged by automated anti-bot systems, or silently returns empty arrays because the search engine rolled out an unannounced layout tweak.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Environment&lt;/th&gt;
&lt;th&gt;Execution Path&lt;/th&gt;
&lt;th&gt;Production Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Localhost (Prototyping)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Playwright ==&amp;gt; Residential Home IP ==&amp;gt; Google&lt;/td&gt;
&lt;td&gt;🟢 &lt;strong&gt;200 OK&lt;/strong&gt; (5 results parsed successfully)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Server (DIY)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Playwright ==&amp;gt; Datacenter Cloud IP ==&amp;gt; Google&lt;/td&gt;
&lt;td&gt;🔴 &lt;strong&gt;429 Blocked / CAPTCHA Wall&lt;/strong&gt; (Scraper fails)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Managed API (SearchApi)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;App Worker ==&amp;gt; HTTP GET ==&amp;gt; SearchApi Infrastructure&lt;/td&gt;
&lt;td&gt;⚡ &lt;strong&gt;200 OK&lt;/strong&gt; (Instant structured JSON payload)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;While headless browsers are exceptional tools for end-to-end web testing and scraping low-security websites, using them to scrape &lt;a href="https://www.coursera.org/articles/what-is-serp" rel="noopener noreferrer"&gt;Search Engine Results Pages (SERPs)&lt;/a&gt; is an expensive engineering trap.&lt;/p&gt;

&lt;p&gt;In this guide, we break down the hidden operational costs of DIY browser automation, compare side-by-side Python implementations, and explore why switching to a dedicated SERP API like &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt; saves your engineering team time, infrastructure budget, and maintenance headaches.&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%2Frsdggff2zc51dajtq2uo.jpeg" 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%2Frsdggff2zc51dajtq2uo.jpeg" alt="Headless browsers vs. SERP APIs: Why your Playwright scraper will fail in production" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost of doing it yourself
&lt;/h2&gt;

&lt;p&gt;Developers naturally enjoy building systems from the ground up. However, building a custom search engine scraper means entering an ongoing cat-and-mouse game against some of the most &lt;a href="https://www.broworks.net/blog/googles-anti-scraping-crackdown-shakes-seo-tools-worldwide" rel="noopener noreferrer"&gt;sophisticated anti-bot security&lt;/a&gt; teams in the world.&lt;/p&gt;

&lt;p&gt;When you scale a headless browser scraper beyond a handful of casual requests, you immediately run into four severe architectural bottlenecks:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    Root["DIY Browser Scraping Bottlenecks"] --&amp;gt; B1["1. Proxy Infrastructure Tax&amp;lt;br/&amp;gt;High residential bandwidth costs"]
    Root --&amp;gt; B2["2. Fragile Selector Maintenance&amp;lt;br/&amp;gt;Random CSS class changes break code"]
    Root --&amp;gt; B3["3. Server Resource Drain&amp;lt;br/&amp;gt;150MB+ RAM per tab, CPU spikes"]
    Root --&amp;gt; B4["4. Anti-Bot Fingerprinting&amp;lt;br/&amp;gt;TLS, Canvas, WebGL, JA3 detection"]&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  1. The proxy infrastructure tax
&lt;/h3&gt;

&lt;p&gt;Search engines can easily identify and block automated requests originating from data center IP ranges (such as AWS, Google Cloud Platform, or DigitalOcean). If you send five consecutive queries from an AWS EC2 instance without a proxy, your IP address is flagged almost immediately.&lt;/p&gt;

&lt;p&gt;To bypass this restriction, you must purchase and configure a residential proxy network. Residential proxies route your requests through real residential consumer connections, making them appear legitimate. However, managing proxies introduces heavy technical complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bandwidth costs&lt;/strong&gt;: Residential proxy providers charge based on data transfer, usually between $5 and $15 per gigabyte. When a headless browser loads a search results page, it downloads not just the text, but also JavaScript bundles, stylesheets, tracking pixels, and images. Loading these unnecessary assets inflates your proxy bandwidth bill significantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pool rotation and stickiness&lt;/strong&gt;: You must write custom pooling logic to rotate IP addresses across queries while maintaining sticky sessions when handling multi-page pagination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gateway latency&lt;/strong&gt;: Routing traffic through multiple proxy hops adds anywhere from 1 to 4 seconds of network latency to every single request.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Fragile HTML selector maintenance
&lt;/h3&gt;

&lt;p&gt;Search engines do not use semantic, human-readable HTML markup. Instead of providing clear class names like &lt;code&gt;.search-result-item&lt;/code&gt; or &lt;code&gt;.result-title&lt;/code&gt;, they rely on minified, randomized, or hashed CSS selectors that change frequently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- What you hope to see in the DOM --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"search-result"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Web Scraping Guide&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"snippet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Learn how to extract data...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- What Google actually renders --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"MjjYud"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"g Ww4FFb vt6AZc"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"kvH3df"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"VwiC3b yD755b xDu2fd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Learn how to extract data...&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your code relies on fragile selectors like &lt;code&gt;div.g&lt;/code&gt; or &lt;code&gt;.VwiC3b&lt;/code&gt;, an unannounced A/B test or markup refresh by the search engine will cause your parser to fail silently. You will receive empty datasets, triggering alerts and requiring developers to drop their current sprint tasks to inspect DOM trees and update CSS queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Massive server resource drain
&lt;/h3&gt;

&lt;p&gt;Headless browsers are full browser execution environments. Running an instance of headless Chromium or Firefox forces your host server to parse HTML, evaluate complex JavaScript scripts, construct DOM trees, and calculate CSS layout rules.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory consumption&lt;/strong&gt;: A single headless browser tab typically consumes between 150MB and 300MB of RAM. If your application needs to handle 30 concurrent search queries, your server needs at least 8GB to 16GB of dedicated RAM just to keep the browser processes alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU spikes&lt;/strong&gt;: Initializing browser contexts and rendering client-side JavaScript creates sharp CPU spikes, often requiring expensive multi-core cloud compute instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process zombie leaks&lt;/strong&gt;: Long-running browser processes frequently experience memory leaks. Without aggressive lifecycle management and process termination routines, orphaned Chromium processes will gradually consume all available server memory and crash your host container.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Advanced anti-bot fingerprinting and CAPTCHAs
&lt;/h3&gt;

&lt;p&gt;Modern search engines employ sophisticated behavioral and cryptographic bot detection techniques. They do not just check your User-Agent header; they inspect deep browser characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TLS and JA3/JA4 fingerprinting&lt;/strong&gt;: The way your networking stack negotiates SSL/TLS handshakes reveals whether your request comes from a genuine desktop browser or an automated runtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Navigator and JavaScript runtime properties&lt;/strong&gt;: Automated environments often leak telltale attributes such as &lt;code&gt;navigator.webdriver = true&lt;/code&gt;, missing system plugin arrays, or default WebGL vendor strings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canvas and audio fingerprinting&lt;/strong&gt;: Scripts render hidden shapes or audio signals to generate a unique hardware signature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavioral heuristics&lt;/strong&gt;: Mouse movement velocity, keystroke timings, and scroll dynamics are analyzed in real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even when using stealth plugins like &lt;code&gt;puppeteer-extra-plugin-stealth&lt;/code&gt; or custom Playwright evasion flags, these countermeasures are brittle. Anti-bot vendors continuously update their detection models, leaving your custom scraper vulnerable to sudden blocks and CAPTCHA walls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The abstracted alternative: Dedicated SERP APIs
&lt;/h2&gt;

&lt;p&gt;A dedicated Search Engine Results Page (SERP) API transforms search engine data collection into a fully managed infrastructure service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fevhjogolxocysfqkhzlz.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fmedia%2Fiij6ovinfhm.png" alt="SearchApi.io logo" width="800" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of launching browsers, rotating proxies, handling CAPTCHAs, and maintaining brittle DOM selectors, you offload the entire operational pipeline to a specialized provider like &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    App["Your Application"] --&amp;gt;|1. Simple HTTP GET| API["SearchApi.io Engine"]

    subgraph ManagedInfra["Managed Infrastructure Layer"]
        API --&amp;gt; Proxies["Residential &amp;amp; Mobile Proxies"]
        API --&amp;gt; Browsers["Stealth Browser Fleet"]
        API --&amp;gt; Parsers["Auto-Updating DOM Parsers"]
    end

    ManagedInfra --&amp;gt;|2. Automated Request| Google["Search Engines (Google, Bing)"]
    Google --&amp;gt;|3. Live SERP Response| ManagedInfra
    API --&amp;gt;|4. Clean Structured JSON| App&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt; handles every layer of the extraction process behind a clean REST Application Programming Interface (API):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent proxy rotation&lt;/strong&gt;: Automatically routes queries through optimized residential and mobile IP pools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fingerprint emulation&lt;/strong&gt;: Emulates authentic browser signatures at both the network (TLS/HTTP2) and JavaScript runtime levels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated parsing&lt;/strong&gt;: Upstream parsers continuously monitor search engine layout updates. When Google changes a CSS class, &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt; updates its internal parsers immediately, ensuring your application always receives structured, consistent JSON without code modifications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightweight delivery&lt;/strong&gt;: Your server sends a lightweight HTTP request and receives clean, parsed JSON in milliseconds, using minimal CPU and memory.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Side-by-side implementation: Playwright vs. SearchApi
&lt;/h2&gt;

&lt;p&gt;To see the difference in code simplicity, maintainability, and resource footprint, let us compare two complete Python implementations designed to accomplish the exact same task: searching Google for &lt;code&gt;"Top web scraping frameworks in 2026"&lt;/code&gt; and extracting the top organic results (position, title, URL, and snippet text).&lt;/p&gt;

&lt;h3&gt;
  
  
  Building a custom Playwright scraper in Python
&lt;/h3&gt;

&lt;p&gt;The following script represents the DIY approach. It launches an automated Chromium browser, configures viewport parameters, attempts to bypass basic bot flags, waits for network idle states, and parses HTML using CSS selectors.&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
DIY Google Search Scraper using Playwright
Requires: pip install playwright
Run: playwright install chromium
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.async_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;async_playwright&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scrape_google_diy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_query&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&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="n"&gt;Any&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;Launching headless Chromium for query: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;search_query&lt;/span&gt;&lt;span class="si"&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="n"&gt;search_results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&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="n"&gt;Any&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;async_playwright&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;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Launch Chromium with anti-detection flags
&lt;/span&gt;        &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;headless&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;args&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;--disable-blink-features=AutomationControlled&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;--no-sandbox&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;--disable-setuid-sandbox&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;--disable-infobars&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;--window-size=1920,1080&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="c1"&gt;# Configure browser context with realistic desktop parameters
&lt;/span&gt;        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;user_agent&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;Mozilla/5.0 (Windows NT 10.0; Win64; x64) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AppleWebKit/537.36 (KHTML, like Gecko) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chrome/124.0.0.0 Safari/537.36&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;viewport&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;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timezone_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;America/New_York&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;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Format Google search URL
&lt;/span&gt;        &lt;span class="n"&gt;encoded_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;search_query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&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="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="n"&gt;url&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="s"&gt;https://www.google.com/search?q=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;encoded_query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;hl=en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Navigate and wait for DOM network idle state
&lt;/span&gt;            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&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;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&lt;/span&gt;&lt;span class="sh"&gt;"&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;30000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Define fragile CSS selectors for organic search results
&lt;/span&gt;            &lt;span class="n"&gt;organic_container_selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;div.g, div.MjjYud&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;organic_container_selector&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;7000&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Query all potential organic result cards
&lt;/span&gt;            &lt;span class="n"&gt;containers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_selector_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;organic_container_selector&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;# Extract title from h3 tag
&lt;/span&gt;                &lt;span class="n"&gt;title_elem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="c1"&gt;# Extract link anchor tag
&lt;/span&gt;                &lt;span class="n"&gt;link_elem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_selector&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="c1"&gt;# Extract snippet text using known class heuristics
&lt;/span&gt;                &lt;span class="n"&gt;snippet_elem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;div.VwiC3b, div[style*=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;-webkit-line-clamp&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="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;title_elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&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;title_elem&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;N/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;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;link_elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_attribute&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;link_elem&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;N/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;snippet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;snippet_elem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&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;snippet_elem&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;N/A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="c1"&gt;# Filter out nested navigational links and empty blocks
&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;title&lt;/span&gt; &lt;span class="o"&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="ow"&gt;and&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;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;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google.com&lt;/span&gt;&lt;span class="sh"&gt;"&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;link&lt;/span&gt;
                &lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;search_results&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="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;position&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="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;snippet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;snippet&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;position&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

                &lt;span class="k"&gt;if&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;search_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&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;Scraping failed due to selector timeout or block: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;search_results&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&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="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Top web scraping frameworks in 2026&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;scrape_google_diy&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="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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Retrieved &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;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; results via Playwright:&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;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;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;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;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="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;    Link: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="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="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The streamlined SearchApi approach
&lt;/h3&gt;

&lt;p&gt;Now, let us examine the equivalent implementation using &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt;. Notice how all browser lifecycle management, selector queries, and network evasions disappear.&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Production-ready Google Search Extraction using SearchApi
Requires: pip install requests python-dotenv
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&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;scrape_google_with_searchapi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;search_query&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="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;United States&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;desktop&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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&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="n"&gt;Any&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;Requesting structured payload for: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;search_query&lt;/span&gt;&lt;span class="si"&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="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="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEARCHAPI_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;api_key&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;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing SEARCHAPI_API_KEY environment variable.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# SearchApi Google Search endpoint
&lt;/span&gt;    &lt;span class="n"&gt;endpoint&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.searchapi.io/api/v1/search&lt;/span&gt;&lt;span class="sh"&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;google&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="n"&gt;search_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;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;api_key&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="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;device&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hl&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;en&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;gl&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;us&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;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;endpoint&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;15&lt;/span&gt;&lt;span class="p"&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;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;payload&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="n"&gt;organic_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&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;organic_results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

        &lt;span class="n"&gt;extracted_data&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;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;organic_results&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="n"&gt;extracted_data&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="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;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;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;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;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="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;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;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;displayed_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;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;displayed_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;snippet&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="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;snippet&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;extracted_data&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;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;http_err&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;SearchApi HTTP error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;http_err&lt;/span&gt;&lt;span class="p"&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;http_err&lt;/span&gt;&lt;span class="p"&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="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="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;exceptions&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="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;Network request error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&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="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Top web scraping frameworks in 2026&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scrape_google_with_searchapi&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;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;New York, New York, United States&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Retrieved &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;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; results via SearchApi:&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;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;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;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;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="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;    Link: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;    Display: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;displayed_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="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Code walkthrough and key differences
&lt;/h3&gt;

&lt;p&gt;Let us break down what happens behind the scenes in both codebases:&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;DIY Playwright Scraper&lt;/th&gt;
&lt;th&gt;Managed SearchApi Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~100 lines of boilerplate&lt;/td&gt;
&lt;td&gt;~35 lines of standard Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Playwright + Chromium binary (~300MB)&lt;/td&gt;
&lt;td&gt;Standard HTTP client (&lt;code&gt;requests&lt;/code&gt; &amp;lt; 5MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Footprint (RAM)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;150MB - 300MB per active tab&lt;/td&gt;
&lt;td&gt;&amp;lt; 10MB per HTTP worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance &amp;amp; Recovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual DOM re-inspection on layout updates&lt;/td&gt;
&lt;td&gt;Automatic upstream parser updates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Lines of code and complexity&lt;/strong&gt;: The Playwright script requires over 90 lines of boilerplate, including browser process configuration, context options, selector query strings, element null-checks, and error catching. The SearchApi implementation requires under 35 lines of standard, clean Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary footprint&lt;/strong&gt;: Running Playwright in a Docker container or serverless function requires downloading and bundling a full Chromium binary (~300MB). SearchApi runs with a lightweight HTTP client like &lt;code&gt;requests&lt;/code&gt; or &lt;code&gt;httpx&lt;/code&gt; (&amp;lt; 5MB total dependency size).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data format integrity&lt;/strong&gt;: In the Playwright snippet, if Google alters &lt;code&gt;.VwiC3b&lt;/code&gt; to a new CSS class name, the script returns &lt;code&gt;N/A&lt;/code&gt; for all snippets without throwing a visible network error. In contrast, SearchApi returns a normalized JSON object where fields like &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;link&lt;/code&gt;, &lt;code&gt;snippet&lt;/code&gt;, &lt;code&gt;displayed_link&lt;/code&gt;, and &lt;code&gt;sitelinks&lt;/code&gt; remain consistent regardless of Google's internal layout tests.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Technical performance and cost showdown
&lt;/h2&gt;

&lt;p&gt;When deciding between building an internal headless browser cluster and integrating an API, you must evaluate total operational overhead across compute, proxies, reliability, and engineering salaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detailed architectural comparison
&lt;/h3&gt;

&lt;p&gt;The table below highlights the technical trade-offs between both approaches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature / Metric&lt;/th&gt;
&lt;th&gt;DIY Headless Browsers (Playwright / Puppeteer)&lt;/th&gt;
&lt;th&gt;Dedicated SERP API (SearchApi)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Initial Setup Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Days to weeks (proxy setup, evasion scripting)&lt;/td&gt;
&lt;td&gt;5 minutes (API key integration)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance Overhead&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (ongoing selector patches and bot updates)&lt;/td&gt;
&lt;td&gt;Zero (handled upstream by provider)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory Usage (RAM)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;150MB - 300MB per concurrent tab&lt;/td&gt;
&lt;td&gt;&amp;lt; 10MB per HTTP worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU Utilization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (JavaScript parsing and layout rendering)&lt;/td&gt;
&lt;td&gt;Minimal (JSON deserialization)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bandwidth per Query&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.5MB - 3.5MB (full HTML, CSS, JS assets)&lt;/td&gt;
&lt;td&gt;15KB - 40KB (compressed JSON response)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Proxy Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual (residential pools, rotation, sticky IPs)&lt;/td&gt;
&lt;td&gt;Fully managed and automated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CAPTCHA Resolution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires third-party solvers or browser hooks&lt;/td&gt;
&lt;td&gt;Handled automatically with 99.9% success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Raw HTML / Unstructured DOM elements&lt;/td&gt;
&lt;td&gt;Pre-parsed, structured JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complex (orchestrating browser clusters on Kubernetes)&lt;/td&gt;
&lt;td&gt;Seamless (call API endpoints with high concurrency)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Availability / SLA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unpredictable (prone to sudden bans)&lt;/td&gt;
&lt;td&gt;Guaranteed 99.9% uptime SLA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Real-world cost breakdown at scale
&lt;/h3&gt;

&lt;p&gt;Many engineering teams assume that DIY scraping is "free" because open-source tools like Playwright cost nothing to download. However, hosting and maintaining a high-volume scraper reveals significant hidden costs.&lt;/p&gt;

&lt;p&gt;Let us model the estimated monthly expense of running &lt;strong&gt;100,000 search queries per month&lt;/strong&gt; across both architectures:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph DIY["DIY Headless Browsers: ~$2,296 / month"]
        D_Proxies["Residential Proxies (200GB @ $8/GB)&amp;lt;br/&amp;gt;$1,600"]
        D_Eng["Engineering Maintenance (6 hrs @ $90/hr)&amp;lt;br/&amp;gt;$540"]
        D_Compute["Cloud Compute (2x 8GB Instances)&amp;lt;br/&amp;gt;$96"]
        D_Captcha["CAPTCHA Solver Service&amp;lt;br/&amp;gt;$60"]
    end

    subgraph API["Managed SERP API (SearchApi): $250 / month"]
        A_Tier["100k Searches (BigData Plan)&amp;lt;br/&amp;gt;$250"]
        A_Maint["Zero Infrastructure &amp;amp; Maintenance Overhead&amp;lt;br/&amp;gt;$0"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Proxy bandwidth amplification&lt;/strong&gt;: Because headless browsers download client assets, 100k queries easily consume 200GB+ of residential proxy bandwidth. At $8 per GB, proxies alone cost $1,600 per month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server infrastructure&lt;/strong&gt;: Running 20 to 30 concurrent Playwright instances requires dedicated virtual machines (e.g. AWS EC2 &lt;code&gt;t4g.xlarge&lt;/code&gt; or &lt;code&gt;c6i.xlarge&lt;/code&gt;), costing roughly $96 to $140 per month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering time&lt;/strong&gt;: When selectors break twice a month, an engineer spends 3 to 4 hours diagnosing issues, testing selectors, and deploying hotfixes. At an average developer cost of $90 per hour, maintenance costs exceed $540 every month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;By contrast, using a managed SERP API&lt;/strong&gt; (such as SearchApi's BigData Plan at $250 per month for 100,000 searches) consolidates your entire bill into a single, predictable subscription with a 99.9% SLA, saving over $2,000 every month while eliminating developer on-call fatigue.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Handling advanced search engine layouts and rich snippets
&lt;/h2&gt;

&lt;p&gt;Modern search results are no longer simple lists of ten blue links. Search engines present dynamic, highly interactive features designed to answer user questions directly on the page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;People Also Ask (PAA)&lt;/strong&gt;: Accordion dropdowns that load dynamic content via asynchronous JavaScript calls upon being clicked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge Graph panels&lt;/strong&gt;: Rich sidebars containing entity facts, social profiles, founders, and related topics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Maps and local packs&lt;/strong&gt;: Interactive business listings with customer review ratings, operating hours, and geocoordinates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Overviews and rich snippets&lt;/strong&gt;: Dynamically streamed answers, product price tags, ratings, and video carousels.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    SERP["Search Engine Results Page (SERP)"] --&amp;gt; AI["AI Overview / Rich Answer Box&amp;lt;br/&amp;gt;(Dynamic streaming content)"]
    SERP --&amp;gt; Main["Main Results Stream"]
    SERP --&amp;gt; Side["Entity Sidebar"]

    Main --&amp;gt; PAA["People Also Ask (PAA)&amp;lt;br/&amp;gt;&amp;lt;i&amp;gt;(Interactive JavaScript accordions)&amp;lt;/i&amp;gt;"]
    Main --&amp;gt; Maps["Local 3-Pack / Google Maps&amp;lt;br/&amp;gt;(Dynamic scroll &amp;amp; geocoordinates)"]
    Main --&amp;gt; Organic["Organic Search Results&amp;lt;br/&amp;gt;(Titles, URLs, sitelinks, snippets)"]

    Side --&amp;gt; KG["Knowledge Graph Panel&amp;lt;br/&amp;gt;(Entity attributes, social links, facts)"]&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  The challenge with DIY extraction of interactive elements
&lt;/h3&gt;

&lt;p&gt;Extracting these advanced components with Playwright is remarkably complex:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To scrape &lt;strong&gt;People Also Ask&lt;/strong&gt; questions, your script must locate each accordion element, issue click actions to trigger JavaScript expand events, wait for animation frames to finish, and parse the newly inserted DOM nodes.&lt;/li&gt;
&lt;li&gt;To scrape &lt;strong&gt;Local Maps packs&lt;/strong&gt;, your script must interact with map containers, handle scrolling viewports, and extract coordinates embedded inside nested JavaScript payloads.&lt;/li&gt;
&lt;li&gt;To scrape &lt;strong&gt;Knowledge Graph boxes&lt;/strong&gt;, your selectors must account for dozens of entity variations (companies, celebrities, movies, books, and locations), each of which renders with different HTML tags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writing and maintaining code for all these edge cases requires hundreds of lines of brittle automation logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extracting structured SERP data effortlessly
&lt;/h3&gt;

&lt;p&gt;With SearchApi, extracting interactive widgets requires zero browser actions. The API automatically parses every SERP feature into clean, dedicated JSON objects:&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;"search_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;"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;"search_65df89bc12e4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&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-08-29T05:12:00.000Z"&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_time_taken"&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.62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parsing_time_taken"&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.22&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_time_taken"&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.84&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;"ai_overview"&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;"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;"Playwright is an open-source automation library and framework created by Microsoft for end-to-end testing across Chromium, Firefox, and WebKit."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reference_links"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Playwright: Fast and reliable end-to-end testing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://playwright.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"playwright.dev"&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;"knowledge_graph"&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;"kgmid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/g/11f555cn8l"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"knowledge_graph_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;"Software library"&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;"Playwright"&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;"Software library"&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;"Playwright is an open-source automation library for browser testing and web scraping developed by Microsoft."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"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;"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://en.wikipedia.org/wiki/Playwright_(software)"&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;"website"&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://playwright.dev"&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;"related_questions"&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;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Is Playwright better than Selenium?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Playwright offers faster execution, native auto-waiting, built-in async support, and modern dev tools integration compared to Selenium."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&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;"Playwright vs Selenium: Modern Web Automation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/playwright-vs-selenium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"displayed_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://example.com &amp;gt; playwright-vs-selenium"&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;"organic_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;"Playwright: Fast and reliable end-to-end testing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://playwright.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"displayed_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://playwright.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"playwright.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"snippet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Playwright enables reliable end-to-end testing for modern web apps across all modern rendering engines."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sitelinks"&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;"inline"&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="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;"Docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://playwright.dev/docs/intro"&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="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;"Python API"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"link"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://playwright.dev/python/docs/intro"&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="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;Your data pipeline can instantly access &lt;code&gt;payload["ai_overview"]["markdown"]&lt;/code&gt;, &lt;code&gt;payload["knowledge_graph"]&lt;/code&gt;, or &lt;code&gt;payload["related_questions"]&lt;/code&gt; without having to simulate user clicks or manage animation delays.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision matrix: Choosing the right tool for your project
&lt;/h2&gt;

&lt;p&gt;Headless browsers and SERP APIs both have distinct places in modern software engineering. The key is applying the right tool to the right problem.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    Start{"What are you scraping?"}

    Start --&amp;gt;|Search Engine Results&amp;lt;br/&amp;gt;Google, Bing, Yahoo, Baidu| SERPBranch["Use a Dedicated SERP API&amp;lt;br/&amp;gt;(e.g. SearchApi)"]
    Start --&amp;gt;|General Web Pages&amp;lt;br/&amp;gt;Internal tools, small blogs, custom apps| WebBranch["Use Headless Browsers&amp;lt;br/&amp;gt;(Playwright, Puppeteer)"]

    SERPBranch --&amp;gt; S1["Fast, pre-parsed structured JSON"]
    SERPBranch --&amp;gt; S2["Zero proxy or CAPTCHA management"]
    SERPBranch --&amp;gt; S3["Guaranteed 99.9% uptime SLA"]

    WebBranch --&amp;gt; W1["Full UI interaction &amp;amp; custom click flows"]
    WebBranch --&amp;gt; W2["Session &amp;amp; SSO authentication handling"]
    WebBranch --&amp;gt; W3["Visual testing &amp;amp; PDF/screenshot generation"]&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  When to use headless browsers (Playwright, Puppeteer, Selenium)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end application testing&lt;/strong&gt;: Automating user journeys, validating form submissions, and testing web applications in CI/CD pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scraping authenticated enterprise portals&lt;/strong&gt;: Logging into internal corporate dashboards or web apps behind custom Single Sign-On (SSO) login systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual regression testing and screenshots&lt;/strong&gt;: Capturing full-page screenshots, rendering PDFs, and verifying UI visual consistency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-security, dynamic niche sites&lt;/strong&gt;: Extracting content from single-page web applications (SPAs) that do not enforce strict anti-bot measures.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When to use a dedicated SERP API (SearchApi)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SEO rank tracking and SERP monitoring&lt;/strong&gt;: Tracking organic rankings, featured snippets, and local search visibility for thousands of keywords daily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agent web search and RAG pipelines&lt;/strong&gt;: Providing real-time search capabilities and clean web context to Large Language Models (LLMs) without latency bottlenecks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-commerce price intelligence and Google Shopping&lt;/strong&gt;: Extracting structured product pricing, seller ratings, and stock availability at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market research and competitor tracking&lt;/strong&gt;: Monitoring news trends, brand mentions, and Knowledge Graph developments with guaranteed uptime.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Building your own search engine scraper with Playwright feels like a quick win during prototyping. But as your project scales, the hidden costs of residential proxies, anti-bot defenses, fragile DOM selectors, and heavy server hardware turn a simple script into a continuous infrastructure burden.&lt;/p&gt;

&lt;p&gt;Developer time is your team's most valuable asset. Spending hours debugging broken CSS selectors and configuring proxy rotation takes engineering focus away from building core features that deliver real customer value.&lt;/p&gt;

&lt;p&gt;By shifting your search data collection to a managed service like &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt;, you eliminate infrastructure complexity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Replace 100+ lines of fragile automation scripts with a clean, single HTTP GET call.&lt;/li&gt;
&lt;li&gt;Reduce cloud compute and proxy bandwidth expenses by over 80%.&lt;/li&gt;
&lt;li&gt;Gain instant access to structured JSON data for organic results, People Also Ask questions, Knowledge Graphs, and local packs.&lt;/li&gt;
&lt;li&gt;Enjoy guaranteed 99.9% uptime backed by enterprise-grade proxy rotation and automatic parser maintenance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stop wrestling with headless browser memory leaks and CAPTCHA blocks. &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;Sign up for SearchApi&lt;/a&gt;, claim your free API credits, and start extracting reliable search engine data in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading and references
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://www.guvi.in/blog/what-is-web-scraping-and-how-to-use-it/" rel="noopener noreferrer"&gt;Web Scraping Architecture and Techniques&lt;/a&gt; - Overview of web data collection patterns.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://automatetheboringstuff.com/3e/chapter13.html" rel="noopener noreferrer"&gt;Automate the Boring Stuff with Python&lt;/a&gt; - Fundamentals of web scraping and browser control.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.browserless.io/blog/observations-running-headless-browser" rel="noopener noreferrer"&gt;Observations from Running Headless Browsers&lt;/a&gt; - Deep dive into memory, CPU, and process management.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://latenode.com/blog/selenium-headless" rel="noopener noreferrer"&gt;Headless Selenium and Playwright Operations&lt;/a&gt; - Technical trade-offs of headless automation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nimbleway.com/blog/what-is-a-headless-browser" rel="noopener noreferrer"&gt;Understanding Modern Headless Browsers&lt;/a&gt; - Guide to browser architectures and anti-bot systems.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://onlinesuccessgenie.com/web-scraping-without-browser/" rel="noopener noreferrer"&gt;Web Scraping Without a Browser&lt;/a&gt; - Strategies for reducing compute overhead with APIs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://brightdata.com/blog/web-data/proxy-scraping-vs-scraping-browser" rel="noopener noreferrer"&gt;Proxy Scraping vs. Scraping Browsers&lt;/a&gt; - Comprehensive proxy comparison and bandwidth analysis.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://crawlbase.com/blog/headless-browsers-vs-api-scraping/" rel="noopener noreferrer"&gt;Headless Browsers vs. API Scraping&lt;/a&gt; - Performance and cost evaluation for web scraping.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://scrapingant.com/blog/google-web-scraping-alternatives" rel="noopener noreferrer"&gt;Alternatives for Google Search Data Extraction&lt;/a&gt; - Evaluating scraping proxies and SERP APIs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.designrush.com/agency/it-services/trends/serp-scraper-api" rel="noopener noreferrer"&gt;Search Engine Scraping Trends and Tools&lt;/a&gt; - Industry benchmarks for SERP data collection.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>playwright</category>
      <category>dataextraction</category>
      <category>serpapi</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Building a real-time AI search agent with SearchApi and OpenAI</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Fri, 21 Aug 2026 04:38:31 +0000</pubDate>
      <link>https://dev.to/eunit/building-a-real-time-ai-search-agent-with-searchapi-and-openai-16g8</link>
      <guid>https://dev.to/eunit/building-a-real-time-ai-search-agent-with-searchapi-and-openai-16g8</guid>
      <description>&lt;p&gt;Large Language Models (LLMs) are remarkably capable, but they suffer from two fundamental flaws: &lt;a href="https://arxiv.org/html/2603.08274" rel="noopener noreferrer"&gt;knowledge cutoffs and hallucinations&lt;/a&gt;. Ask an offline model about a breaking news event, a shifting stock price, or a new library release that dropped this morning, and it will either confidently fabricate a wrong answer or admit it has no idea. To build production-grade AI tools, your models need eyes and ears on the live web; your models need to be in the know of happenings as they break.&lt;/p&gt;

&lt;p&gt;While static &lt;a href="https://aws.amazon.com/what-is/retrieval-augmented-generation/" rel="noopener noreferrer"&gt;Retrieval-Augmented Generation (RAG)&lt;/a&gt; works well for searching internal vector databases, it fails the moment an accurate answer requires a real-time Google search. That is where AI search agents come in. An autonomous search agent turns an LLM into a reasoning engine that can choose when to browse the web, pull down search engine results pages (SERPs), synthesize real-time data, and deliver accurate, fact-checked answers.&lt;/p&gt;

&lt;p&gt;In this article, you will build a production-ready AI search agent from scratch using Python, OpenAI's tool-calling framework, and &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt; as the live web data pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why traditional web scraping fails for AI agents
&lt;/h2&gt;

&lt;p&gt;When developers realize their AI agent needs Google data, the first instinct is usually to reach for raw browser automation tools like &lt;a href="https://www.eunit.me/blog/playwright-vs-webdriverio-uncovering-the-differences" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt;, Puppeteer, or BeautifulSoup. You write code to open a headless browser, navigate to Google, type a query, and scrape the HTML.&lt;/p&gt;

&lt;p&gt;In a local testing environment, this approach works for about three requests. In production, it breaks immediately.&lt;/p&gt;

&lt;p&gt;Building your own search engine scraper means constantly fighting multi-billion-dollar anti-bot security teams. Your script will quickly run into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP blocks and CAPTCHAs:&lt;/strong&gt; Search engines rapidly flag and block data center IP addresses. Rotating your own proxy pool is expensive and time-consuming to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fragile DOM structures:&lt;/strong&gt; Search engines constantly shift their HTML layout and CSS classes. A script targeting &lt;code&gt;div.g&lt;/code&gt; tonight will silently return empty data by tomorrow morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute overhead:&lt;/strong&gt; Spinning up heavy browser environments (like headless Chromium) consumes significant server memory and CPU cycles, adding cost and latency to every query.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To keep AI agents fast and reliable, you need an abstraction layer that handles proxy rotation, bypasses CAPTCHAs, and returns structured data instantly. By offloading this infrastructure to &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt;, you retrieve clean, pre-parsed JSON payloads using a lightweight HTTP request - at a fraction of the compute overhead.&lt;/p&gt;

&lt;h2&gt;
  
  
  System architecture
&lt;/h2&gt;

&lt;p&gt;Before writing any code, it helps to understand how an autonomous tool-calling loop operates. This system does not blindly search the web on every turn. Instead, it follows a four-step conversation loop:&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%2Frug2xhakyk3daovg5jis.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%2Frug2xhakyk3daovg5jis.png" alt="AI Search Agent architecture - 4-step flow: User query to LLM decision to SearchApi call to AI synthesis" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The intent check:&lt;/strong&gt; The user asks a question. The LLM evaluates whether it can answer using its internal knowledge or whether it needs live web data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool call trigger:&lt;/strong&gt; If it needs live data, the LLM stops generating text and outputs a structured request to invoke the &lt;code&gt;search_web&lt;/code&gt; tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The infrastructure layer:&lt;/strong&gt; The Python script intercepts this request, queries SearchApi's Google engine, and returns the top organic search results as structured JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The synthesis:&lt;/strong&gt; The text snippets are injected back into the LLM's context window. The model reads the live web data and generates a grounded, hallucination-free response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This architecture is efficient because the LLM acts as the decision-maker. It avoids unnecessary API calls for questions it can already answer accurately from its training data.&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%2Fevhjogolxocysfqkhzlz.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fmedia%2Fiij6ovinfhm.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%2Fevhjogolxocysfqkhzlz.supabase.co%2Fstorage%2Fv1%2Fobject%2Fpublic%2Fmedia%2Fiij6ovinfhm.png" alt="SearchApi logo" width="800" height="165"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites and project setup
&lt;/h2&gt;

&lt;p&gt;To follow along with this tutorial, you will need three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.8+&lt;/strong&gt; installed on your machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An OpenAI API key&lt;/strong&gt; to power the model's reasoning loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A SearchApi API key&lt;/strong&gt; to handle live web access. You can sign up at &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt; and get 100 free searches right out of the box - no credit card required.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Start by initializing the project folder and installing the required dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create and move into your project folder&lt;/span&gt;
&lt;span class="nb"&gt;mkdir &lt;/span&gt;ai-search-agent &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;ai-search-agent

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;requests openai python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, create a &lt;code&gt;.env&lt;/code&gt; file in the root of your project directory to keep your API tokens secure:&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="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_actual_openai_api_key_here"&lt;/span&gt;
&lt;span class="nv"&gt;SEARCHAPI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_actual_searchapi_api_key_here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the project environment ready, here is an overview of the full project structure you will build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Building a Real-Time AI Search Agent/
├── ai-search-agent/           &amp;lt;- Python CLI Implementation
│   ├── search_helper.py       &amp;lt;- SearchApi wrapper function (Step 1)
│   ├── agent.py               &amp;lt;- OpenAI tool schema and ask_agent() (Step 2)
│   ├── run.py                 &amp;lt;- Orchestrator loop (Step 3)
│   ├── .env.example           &amp;lt;- Template for environment variables
│   └── requirements.txt       &amp;lt;- pip dependencies
└── realtime-search/           &amp;lt;- Production Next.js 14 Application
    ├── app/
    │   ├── api/
    │   │   ├── search/        &amp;lt;- Server-side SearchApi route handler
    │   │   └── synthesize/    &amp;lt;- Real-time OpenAI SSE streaming handler
    │   ├── globals.css        &amp;lt;- Design system tokens &amp;amp; glassmorphism
    │   ├── layout.tsx         &amp;lt;- Root layout &amp;amp; SSR theme script
    │   └── page.tsx           &amp;lt;- Agent dashboard container
    ├── components/            &amp;lt;- React UI components
    ├── hooks/                 &amp;lt;- Pipeline orchestration &amp;amp; history hooks
    ├── lib/                   &amp;lt;- Server utilities &amp;amp; TypeScript definitions
    └── README.md              &amp;lt;- Next.js application documentation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Writing the search API helper function
&lt;/h2&gt;

&lt;p&gt;The first module is a &lt;code&gt;search_web()&lt;/code&gt; function that sends a query to &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi's Google Search engine&lt;/a&gt; and returns a compressed block of text containing the titles, snippets, and source links from the top organic results.&lt;/p&gt;

&lt;p&gt;The key design decision here is to extract only three fields - &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;snippet&lt;/code&gt;, and &lt;code&gt;link&lt;/code&gt; - from SearchApi's &lt;code&gt;organic_results&lt;/code&gt; array. This keeps the payload lightweight and prevents the LLM from wasting tokens on irrelevant metadata like ad blocks, knowledge panels, or navigation breadcrumbs.&lt;/p&gt;

&lt;p&gt;Create a file called &lt;code&gt;search_helper.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;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="c1"&gt;# Load API keys from environment variables
&lt;/span&gt;&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;SEARCHAPI_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="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEARCHAPI_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;def&lt;/span&gt; &lt;span class="nf"&gt;search_web&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;United States&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Queries the SearchApi.io Google Search engine and returns a compressed
    string containing the titles, snippets, and links of organic results.
    &lt;/span&gt;&lt;span class="sh"&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;SEARCHAPI_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error: SearchApi API key missing from environment variables.&lt;/span&gt;&lt;span class="sh"&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://www.searchapi.io/api/v1/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Configure payload parameters according to the SearchApi documentation
&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;google&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="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;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&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;SEARCHAPI_KEY&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;10&lt;/span&gt;&lt;span class="p"&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;raise_for_status&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="c1"&gt;# Extract organic search results
&lt;/span&gt;        &lt;span class="n"&gt;organic_results&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;organic_results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;organic_results&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;No relevant web search results found for: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;

        &lt;span class="c1"&gt;# Parse and stringify the results into a compact context block for the LLM
&lt;/span&gt;        &lt;span class="n"&gt;formatted_results&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;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organic_results&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# Limit to top 5 results
&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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No 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;snippet&lt;/span&gt; &lt;span class="o"&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;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;snippet&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;No Snippet Available&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="o"&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;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="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;formatted_results&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;index&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;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Snippet: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Source: &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="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;return&lt;/span&gt; &lt;span class="sh"&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;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatted_results&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;exceptions&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;e&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;An infrastructure error occurred while querying the search API: &lt;/span&gt;&lt;span class="si"&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;e&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="c1"&gt;# Quick standalone test execution
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;test_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Who won the men&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s 100m sprint in the 2024 Paris Olympics?&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Testing SearchApi wrapper with query: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;test_query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;search_web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_query&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;organic_results[:5]&lt;/code&gt; slice is intentional. Feeding more than five results into the LLM context rarely improves answer quality and meaningfully increases token consumption and cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Defining the LLM agent framework (tool calling)
&lt;/h2&gt;

&lt;p&gt;With the data pipeline ready, you need to teach the LLM when and how to use it. This is done through &lt;a href="https://developers.openai.com/api/docs/guides/function-calling" rel="noopener noreferrer"&gt;OpenAI's &lt;strong&gt;tool calling&lt;/strong&gt;&lt;/a&gt; (formerly called function calling) feature, which lets you define structured schemas that the model can invoke when it determines external data is required.&lt;/p&gt;

&lt;p&gt;Create a file, and name it &lt;code&gt;agent.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;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;search_helper&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;search_web&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize the OpenAI client
&lt;/span&gt;&lt;span class="n"&gt;openai_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&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="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Define the SearchApi tool schema for the model
&lt;/span&gt;&lt;span class="n"&gt;search_tool_definition&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;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;function&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;function&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;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;search_web&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;description&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;Call this tool whenever you need up-to-date information, news, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current events, or web data that occurred after your knowledge cutoff 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;parameters&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;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;object&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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&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;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;string&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;description&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;The optimized search query string to look up on Google &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(e.g. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Tesla Model Y 2026 pricing specifications&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="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="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;string&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;description&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;Geographical location filter for search targeting. Defaults to &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;United States&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&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;United States&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;required&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;query&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="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_prompt&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Sends a query to the LLM along with the available search tool definition.
    The LLM determines autonomously whether it needs to execute a Google search.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;messages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;system&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;content&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;You are a factual, precise AI search agent with live web access via SearchApi. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use the search_web tool when you need current or time-sensitive 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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_prompt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Request model execution with the tools parameter exposed
&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;openai_client&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="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# Efficient, tool-capable model
&lt;/span&gt;        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_tool_definition&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;          &lt;span class="c1"&gt;# Let the model autonomously decide whether to use the tool
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response_message&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;choices&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="n"&gt;message&lt;/span&gt;
    &lt;span class="n"&gt;tool_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_calls&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;Agent decision: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;I need to use SearchApi to look up live information on the web.&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_call_required&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;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message_history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&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;Agent decision: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;I can answer this question from my existing knowledge.&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&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;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response_message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tool_choice="auto"&lt;/code&gt; parameter is the key to the agent's autonomy. It tells OpenAI to let the model decide whether external search is necessary rather than forcing a tool call on every request. Simple questions like "What is Python?" get answered directly, while questions about current events correctly trigger a SearchApi call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Stitching the search agent loop together
&lt;/h2&gt;

&lt;p&gt;The final piece is the orchestrator - a runtime loop that ties everything together. It interprets the model's decision, calls &lt;code&gt;search_web()&lt;/code&gt; with the model's chosen arguments, injects the live results back into the conversation history, and requests a final synthesized answer from OpenAI.&lt;/p&gt;

&lt;p&gt;Create a file called &lt;code&gt;run.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;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ask_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openai_client&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;search_helper&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;search_web&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_prompt&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    The orchestrator runtime loop. It prompts the agent, detects tool requests,
    executes the local SearchApi call, updates the conversation history, and
    generates the final web-informed answer.
    &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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;User: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_prompt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# First turn: ask the agent what it wants to do
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ask_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Case A: The LLM answered immediately without needing the web
&lt;/span&gt;    &lt;span class="k"&gt;if&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;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;completed&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Final answer:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&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;data&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="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;return&lt;/span&gt;

    &lt;span class="c1"&gt;# Case B: The LLM explicitly requested a SearchApi tool call
&lt;/span&gt;    &lt;span class="k"&gt;if&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;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;tool_call_required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tool_calls&lt;/span&gt; &lt;span class="o"&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;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&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;message_history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# Append the model's tool request to keep the chat history balanced
&lt;/span&gt;        &lt;span class="n"&gt;assistant_msg&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;role&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;assistant&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&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;tool_calls&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;function&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;function&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;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;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arguments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&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;tc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;messages&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="n"&gt;assistant_msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Process each tool call (handles potential parallel calls from the model)
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool_call&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;function_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
            &lt;span class="n"&gt;function_args&lt;/span&gt; &lt;span class="o"&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;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&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;Invoking tool: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;function_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;() with arguments &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;function_args&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;if&lt;/span&gt; &lt;span class="n"&gt;function_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_web&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;search_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;function_args&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;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;search_location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;function_args&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;United States&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="c1"&gt;# Fetch structured data live from SearchApi.io
&lt;/span&gt;                &lt;span class="n"&gt;raw_search_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;search_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;search_location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="c1"&gt;# Append the search result payload back to the chat history
&lt;/span&gt;                &lt;span class="n"&gt;tool_response_msg&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;role&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;tool&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;tool_call_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;tool_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;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;function_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw_search_results&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;messages&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="n"&gt;tool_response_msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&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;Warning: model requested unknown tool: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;function_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;return&lt;/span&gt;

        &lt;span class="c1"&gt;# Second turn: send the full conversation history back to OpenAI
&lt;/span&gt;        &lt;span class="c1"&gt;# (This now contains: user query + agent tool call + SearchApi results)
&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;Synthesizing live search data into a comprehensive answer...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;final_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai_client&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="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Final answer:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;final_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&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="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;span class="c1"&gt;# End-to-end system test
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Test case 1: Requires SearchApi intervention (current events)
&lt;/span&gt;    &lt;span class="nf"&gt;run_agent_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What were the top 3 tech stock market trends in Q1 of 2026?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Test case 2: Native knowledge execution (no web tool needed)
&lt;/span&gt;    &lt;span class="nf"&gt;run_agent_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain the fundamental difference between a SQL and NoSQL database in two sentences.&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;Pay close attention to how the &lt;code&gt;messages&lt;/code&gt; list is built across both turns. After the model requests a tool call, you must append the assistant's tool request and then the tool's response before sending the history back to OpenAI. Omitting either step causes the API to return an error - this is the most common breaking point for developers new to tool calling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live test drives
&lt;/h2&gt;

&lt;p&gt;With all three files in place, run the agent with &lt;code&gt;python run.py&lt;/code&gt;. Here is what a real execution looks like for each scenario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario A - Current events query:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: What were the top 3 tech stock market trends in Q1 of 2026?

Agent decision: 'I need to use SearchApi to look up live information on the web.'
Invoking tool: search_web() with arguments {'query': 'top tech stock market trends Q1 2026'}
Synthesizing live search data into a comprehensive answer...

Final answer:
Based on recent web data, the top three tech stock market trends in Q1 2026 were:

1. AI infrastructure stocks surged as hyperscalers accelerated GPU procurement...
2. Semiconductor supply chain stabilization drove AMD and TSMC to multi-year highs...
3. Enterprise SaaS consolidation continued, with several major M&amp;amp;A announcements...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Scenario B - Native knowledge query:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: Explain the fundamental difference between a SQL and NoSQL database in two sentences.

Agent decision: 'I can answer this question from my existing knowledge.'

Final answer:
SQL databases organize data into structured tables with predefined schemas and use
relational joins to link data across tables, making them ideal for consistent,
transactional workloads. NoSQL databases use flexible document, key-value, or graph
data models with no fixed schema, trading strict consistency for horizontal scalability
and performance on unstructured or rapidly evolving data.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent correctly identifies that the first query requires live web data and that the second can be answered from its own training knowledge - skipping an unnecessary API call entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo: see the agent in action
&lt;/h2&gt;

&lt;p&gt;The interactive demo below visualizes the exact agent loop described above - including the tool decision badge, the &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi&lt;/a&gt; result cards, and the synthesized answer panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://realtime-search.vercel.app/" rel="noopener noreferrer"&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%2Fzw44b9zj9qj5b3wdant0.png" alt="AI Search Agent demo - initial state showing the query input and architecture panel" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Interactive Demos &amp;amp; Repositories:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;🚀 Live Web Application:&lt;/strong&gt; Test the full-stack agent live on Vercel at &lt;a href="https://realtime-search.vercel.app/" rel="noopener noreferrer"&gt;realtime-search.vercel.app&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;🐍 Python CLI Repository:&lt;/strong&gt; Clone the Python tutorial source code at &lt;a href="https://github.com/Eunit99/ai-search-agent" rel="noopener noreferrer"&gt;Eunit99/ai-search-agent&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;⚡ Next.js App Repository:&lt;/strong&gt; Clone the production web application code at &lt;a href="https://github.com/Eunit99/realtime-search" rel="noopener noreferrer"&gt;Eunit99/realtime-search&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-Stack Next.js Application:&lt;/strong&gt; Located in &lt;a href="https://github.com/Eunit99/realtime-search" rel="noopener noreferrer"&gt;&lt;code&gt;realtime-search/&lt;/code&gt;&lt;/a&gt; - features server-side API key isolation (&lt;code&gt;/api/search&lt;/code&gt;), real-time OpenAI GPT-4o-mini SSE streaming (&lt;code&gt;/api/synthesize&lt;/code&gt;), geo-targeting, and query history persistence.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Production best practices
&lt;/h2&gt;

&lt;p&gt;Before shipping this agent to production, consider these refinements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filter low-quality snippets to save tokens
&lt;/h3&gt;

&lt;p&gt;SearchApi's &lt;code&gt;organic_results&lt;/code&gt; array occasionally includes results with very short or unhelpful snippets (e.g. login-gated pages or index-only entries). Add a simple length filter to skip any snippet under 40 characters:&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;if&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;snippet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;formatted_results&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;index&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;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Snippet: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Source: &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="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;h3&gt;
  
  
  Use geo-targeting for localized results
&lt;/h3&gt;

&lt;p&gt;SearchApi supports a &lt;code&gt;location&lt;/code&gt; parameter that localizes search results geographically. This is useful for agents that serve users across different regions, where price, news, or availability data differs by market. The &lt;code&gt;search_web()&lt;/code&gt; function already exposes this parameter:&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;search_web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current petrol prices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lagos, Nigeria&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;h3&gt;
  
  
  Cache duplicate queries
&lt;/h3&gt;

&lt;p&gt;In production, users frequently ask similar questions within a short time window. Caching the &lt;code&gt;search_web()&lt;/code&gt; response avoids burning SearchApi credits on identical lookups. A Redis-backed wrapper keeps it simple:&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;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;

&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&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="n"&gt;TTL_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;  &lt;span class="c1"&gt;# Cache results for 5 minutes
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_web_cached&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;United States&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cache_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&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;query&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;location&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;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&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;cache_key&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;cached&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;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_web&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;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TTL_SECONDS&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keep the system prompt specific
&lt;/h3&gt;

&lt;p&gt;Your system prompt directly affects how often the model decides to call &lt;code&gt;search_web&lt;/code&gt;. Be explicit about when the tool should and should not be used:&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use the search_web tool for any question involving current events, prices, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;people, companies, or anything that may have changed after your training cutoff. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Answer directly only for timeless facts, historical events, or technical concepts.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;You have built a production-ready AI search agent that solves the two fundamental limitations of offline LLMs: knowledge cutoffs and hallucinations on real-time data.&lt;/p&gt;

&lt;p&gt;The architecture is intentionally minimal but extensible. The &lt;code&gt;search_web()&lt;/code&gt; function can be swapped for any SearchApi engine (News, YouTube, Google Scholar, and more). The tool schema can be extended with additional tools for math, code execution, or database lookup. The agent loop can be wrapped in a web server, a Slack bot, or a voice assistant.&lt;/p&gt;

&lt;p&gt;The key insight is that SearchApi handles the hard infrastructure work - proxy rotation, CAPTCHA solving, HTML parsing, and structured JSON delivery - so you can focus entirely on the reasoning layer on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ready to build?&lt;/strong&gt; Grab your free API key at &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi.io&lt;/a&gt;, check out the live web app on &lt;a href="https://realtime-search.vercel.app/" rel="noopener noreferrer"&gt;Vercel&lt;/a&gt;, clone the Python CLI repository from &lt;a href="https://github.com/Eunit99/ai-search-agent" rel="noopener noreferrer"&gt;GitHub (ai-search-agent)&lt;/a&gt; or the full-stack web application from &lt;a href="https://github.com/Eunit99/realtime-search" rel="noopener noreferrer"&gt;GitHub (realtime-search)&lt;/a&gt;, and drop a comment describing what you build.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions or improvements? Open an issue or submit a pull request on &lt;a href="https://github.com/Eunit99/ai-search-agent" rel="noopener noreferrer"&gt;GitHub (Python CLI)&lt;/a&gt; or &lt;a href="https://github.com/Eunit99/realtime-search" rel="noopener noreferrer"&gt;GitHub (Next.js App)&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post is sponsored by &lt;a href="https://www.searchapi.io/?utm_source=Dev&amp;amp;utm_medium=Ambassador&amp;amp;utm_campaign=eunit.me" rel="noopener noreferrer"&gt;SearchApi&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>llm</category>
      <category>openai</category>
      <category>searchapi</category>
    </item>
    <item>
      <title>How I automated hyper-local lead routing with Nextdoor Scraper and Slack</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Tue, 04 Aug 2026 16:25:45 +0000</pubDate>
      <link>https://dev.to/eunit/how-i-automated-hyper-local-lead-routing-with-nextdoor-scraper-and-slack-5db0</link>
      <guid>https://dev.to/eunit/how-i-automated-hyper-local-lead-routing-with-nextdoor-scraper-and-slack-5db0</guid>
      <description>&lt;p&gt;Picture this. A sales rep or local contractor sitting in their truck on a Tuesday morning, burning fuel driving 45 minutes across town to quote a home repair, only to discover the prospect is a renter who cannot authorize work. Meanwhile, three streets over, in a hidden, high-density homeowner neighborhood, local residents are actively posting on &lt;a href="https://nextdoor.com/" rel="noopener noreferrer"&gt;Nextdoor&lt;/a&gt; to ask for contractor recommendations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.eunit.me/blog/how-to-scrape-nextdoor-for-hyper-local-demographics-and-community-sentiment" rel="noopener noreferrer"&gt;Collecting hyper-local data into a weekly CSV export&lt;/a&gt; is like brewing a fresh pot of coffee and leaving it on the counter for two days before serving it to your sales team. By the time an analyst manually exports the dataset, filters target zip codes, and emails the report to reps, those golden neighborhood opportunities are cold and competitors have already booked the jobs.&lt;/p&gt;

&lt;p&gt;However, extracting this hyper-local data from public sources has historically presented a severe operational challenge. Nextdoor is a rich ecosystem for local community context, but manually researching across hundreds of city pages is painfully slow. Worse still, traditional web scraping workflows often deposit extracted data into static CSV files or database tables that sit idle until someone manually reviews them days later. In high-intent lead generation, every hour of delay degrades lead momentum and lowers conversion rates.&lt;/p&gt;

&lt;p&gt;To solve this problem, I built two complementary lead routing workflows around &lt;a href="https://apify.com/eunit/nextdoor-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; on the Apify platform. The first is a no-code Slack notification wired directly inside Apify Console. The second is an AI agent workflow that connects Nextdoor Scraper to Claude or Cursor via the &lt;a href="https://mcp.apify.com" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt;, letting an AI agent run the scraper, filter results, and push alerts to Slack through a single conversational prompt.&lt;/p&gt;

&lt;p&gt;In this article, I will walk you through the extraction setup, the qualification filter logic, both Slack routing approaches, and exactly what each option looks like in practice.&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.amazonaws.com%2Fuploads%2Farticles%2Fk3jzl3ho7vzvn5pzdzxa.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.amazonaws.com%2Fuploads%2Farticles%2Fk3jzl3ho7vzvn5pzdzxa.png" alt="Nextdoor logo" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The gap between data collection and sales action
&lt;/h2&gt;

&lt;p&gt;For home service providers, such as &lt;a href="https://blog.apify.com/plumbing-leads/" rel="noopener noreferrer"&gt;plumbers&lt;/a&gt;, roofers, HVAC technicians, &lt;a href="https://blog.apify.com/scraping-real-estate-data/" rel="noopener noreferrer"&gt;landscapers&lt;/a&gt;, and solar installers, speed and geographic precision are everything. Identifying an ideal target neighborhood is only half the battle. If your sales team receives demographic reports 48 hours after a data crawl finishes, the opportunity to run timely local direct mail campaigns, localized digital ad pushes, or door-to-door outreach in high-demand pockets has already started to slip.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------+      +--------------------+      +----------------------+
|  Public Nextdoor  | ---&amp;gt; |  Nextdoor Scraper  | ---&amp;gt; |  Data Dump (CSV/DB)  |
|    City Pages     |      |  (Manual Crawl)    |      |  (Sits Idle Days)    |
+-------------------+      +--------------------+      +----------------------+
                                                                  |
                                                                  v
                                                       [ Delayed Sales Action ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The primary friction point in modern web scraping is not extracting the data - it is the latency between data acquisition and business action. Traditional workflows decouple extraction from execution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A crawler runs and outputs thousands of raw records into key-value stores or CSV files.&lt;/li&gt;
&lt;li&gt;An analyst exports the CSV and manually applies spreadsheet filters to isolate target zip codes.&lt;/li&gt;
&lt;li&gt;The analyst formats a report and emails it to the sales lead.&lt;/li&gt;
&lt;li&gt;The sales lead assigns territories to reps who finally begin outreach.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This multi-step, manual chain creates massive operational friction. By the time reps receive the target list, days have elapsed.&lt;/p&gt;

&lt;p&gt;To eliminate this gap, I built two notification paths that both start from the same Nextdoor Scraper run, and both deliver results to Slack, but in different ways depending on your team's setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------+      +--------------------+      +---------------------+
|  Public Nextdoor  | ---&amp;gt; |  Nextdoor Scraper  | ---&amp;gt; |  Apify Dataset      |
|    City Pages     |      |  (Apify Platform)  |      |  (Structured JSON)  |
+-------------------+      +--------------------+      +---------------------+
                                                                  |
                          +--------------------------+------------+
                          |                          |
                          v                          v
             [ Path A: No-code ]        [ Path B: MCP server ]
             Slack notification         AI agent (Claude/Cursor)
             via Actor integration      calls Nextdoor Scraper,
             inside Apify Console       filters leads, posts to Slack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. The pipeline architecture
&lt;/h2&gt;

&lt;p&gt;The automated lead routing system operates across two stages: extraction and post-run notification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant ND as Public Nextdoor Pages
    participant NS as Nextdoor Scraper (Apify Platform)
    participant DS as Apify Dataset
    participant SL as Slack (#local-leads)
    participant AI as AI Agent (Claude / Cursor)

    ND-&amp;gt;&amp;gt;NS: Fetch public city &amp;amp; neighborhood page HTML
    NS-&amp;gt;&amp;gt;DS: Store structured place data (centroid, income, homeowners)
    Note over DS: Run completes. Dataset is ready.

    alt Path A: No-code Slack notification
        DS-&amp;gt;&amp;gt;SL: Actor integration fires on run finish
        SL--&amp;gt;&amp;gt;DS: Notification delivered
    else Path B: MCP server + AI agent
        AI-&amp;gt;&amp;gt;NS: call-actor via Apify MCP server (mcp.apify.com)
        NS-&amp;gt;&amp;gt;DS: Actor runs and stores results
        DS-&amp;gt;&amp;gt;AI: AI agent reads dataset items
        AI-&amp;gt;&amp;gt;AI: Filter by homeowners &amp;gt;= 50%, income &amp;gt;= $40k
        AI-&amp;gt;&amp;gt;SL: Post formatted alert via Slack bot or Slack MCP tool
    end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stage 1: Extraction
&lt;/h3&gt;

&lt;p&gt;The workflow begins by launching &lt;a href="https://apify.com/eunit/nextdoor-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; on the &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify platform&lt;/a&gt;. The Actor &lt;a href="https://www.eunit.me/blog/how-to-scrape-nextdoor-for-hyper-local-demographics-and-community-sentiment" rel="noopener noreferrer"&gt;targets public Nextdoor city and neighborhood pages&lt;/a&gt;, bypassing the need for complex browser automation setups or raw HTML parsing scripts. It parses visible structured data and metadata, transforming disorganized web content into clean JSON records that include geographic coordinates, demographic metrics, resident counts, and references to community activity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2: Notification routing
&lt;/h3&gt;

&lt;p&gt;Once the Actor run finishes and the dataset is populated, there are two ways to get that data into Slack. The first requires no code at all. The second turns the entire pipeline into something an AI agent like Claude or Cursor can trigger, filter, and broadcast with a single prompt.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Step 1: Extracting targeted neighborhood demographics
&lt;/h2&gt;

&lt;p&gt;To get started, we configure &lt;a href="https://apify.com/eunit/nextdoor-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; to target public city pages. Nextdoor public city pages display valuable summary metrics without requiring user logins or private authentication.&lt;/p&gt;

&lt;h3&gt;
  
  
  Input configuration
&lt;/h3&gt;

&lt;p&gt;In Apify Console, open the &lt;strong&gt;Input&lt;/strong&gt; tab for Nextdoor Scraper and supply your target city URLs along with crawl limits. Here is the &lt;code&gt;JSON&lt;/code&gt; input configuration used for this pipeline:&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;"startUrls"&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://nextdoor.com/city/north-miami--fl/"&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://nextdoor.com/city/bellaire--oh/"&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://nextdoor.com/city/bellbrook--oh/"&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://nextdoor.com/city/belle-center--oh/"&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;"downloadStartUrlsToHtmlSources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxRequestsPerCrawl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&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;"maxItemsPerSection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"followNeighborhoodPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"followBusinessDirectoryPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"followAdditionalResourcePages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"saveHtmlSnapshots"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsjclhz464tfcu05os530.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%2Fsjclhz464tfcu05os530.png" alt="Extracting targeted neighborhood demographics" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Key demographic fields extracted
&lt;/h3&gt;

&lt;p&gt;When Nextdoor Scraper processes a city page, it returns normalized fields ready for immediate analysis:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Sample Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cityName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;Name of the target city or municipality&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"North Miami"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;state&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;Two-letter state abbreviation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"FL"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;centroid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Object&lt;/td&gt;
&lt;td&gt;Geographic center latitude and longitude&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"latitude": 40.016, "longitude": -80.747}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;residentsCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Integer&lt;/td&gt;
&lt;td&gt;Total estimated residents in the area&lt;/td&gt;
&lt;td&gt;&lt;code&gt;59854&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;averageAge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Integer&lt;/td&gt;
&lt;td&gt;Average resident age&lt;/td&gt;
&lt;td&gt;&lt;code&gt;35&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;averageIncome&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;Public average household income indicator&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"$41k"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;percentageHomeowners&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;String&lt;/td&gt;
&lt;td&gt;Share of residents who own their homes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"56%"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;affordabilityScore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Integer&lt;/td&gt;
&lt;td&gt;Calculated local affordability index&lt;/td&gt;
&lt;td&gt;&lt;code&gt;99&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;friendlinessScore&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Integer&lt;/td&gt;
&lt;td&gt;Community friendliness score&lt;/td&gt;
&lt;td&gt;&lt;code&gt;86&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;faqs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Array&lt;/td&gt;
&lt;td&gt;Structured FAQ question and answer pairs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{"question": "...", "answer": "..."}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;neighborhoods&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Array&lt;/td&gt;
&lt;td&gt;Discovered public neighborhood links&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[{"title": "128th St", "url": "..."}]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Sample output JSON
&lt;/h3&gt;

&lt;p&gt;Here is a snippet of the structured dataset item generated by Nextdoor Scraper:&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;"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://nextdoor.com/city/north-miami--fl/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"finalUrl"&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://nextdoor.com/city/north-miami--fl/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pageType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"city"&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;"North Miami, Florida | Nextdoor"&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;"Public city summary extracted from Nextdoor."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cityName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"North Miami"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"centroid"&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;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;25.8901&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-80.1867&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;"residentsCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;59854&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"averageAge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"averageIncome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$48k"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"percentageHomeowners"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"54%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"affordabilityScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"friendlinessScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"neighborhoodCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"businessDirectoryCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"faqCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accessStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public"&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;h3&gt;
  
  
  Cost efficiency with Pay-Per-Event pricing
&lt;/h3&gt;

&lt;p&gt;One major advantage of using &lt;a href="https://apify.com/eunit/nextdoor-scraper" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; on the &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify platform&lt;/a&gt; is its pricing structure. &lt;a href="https://apify.com/eunit/nextdoor-scraper" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; operates on a &lt;a href="https://help.apify.com/en/articles/10700066-what-is-pay-per-event" rel="noopener noreferrer"&gt;Pay-Per-Event (PPE)&lt;/a&gt; model.&lt;/p&gt;

&lt;p&gt;Unlike traditional compute-hour billing where you pay for raw server uptime regardless of success, &lt;a href="https://help.apify.com/en/articles/10700066-what-is-pay-per-event" rel="noopener noreferrer"&gt;PPE&lt;/a&gt; charging means you are billed strictly for successfully extracted place records. If a network fluctuation or access block occurs on a page, you are not billed for that failed attempt. This ensures predictable margins when executing large-scale, automated lead discovery campaigns across thousands of zip codes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Step 2: Filtering for contractor accountability and intent
&lt;/h2&gt;

&lt;p&gt;To understand how conditional filtering works in practice, let us examine a real-world implementation executed during a regional contractor accountability platform pilot with Lake Rock Ventures.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Lake Rock Ventures pilot scenario
&lt;/h3&gt;

&lt;p&gt;Lake Rock Ventures, a venture growth studio, was launching a pilot platform designed to connect vetted local roofing and HVAC contractors with high-intent homeowners. The platform needed to identify suburban communities with high enough homeownership rates to justify targeted direct-response marketing. Sending contractor representatives into renter-heavy neighborhoods resulted in wasted labor costs and low conversion rates.&lt;/p&gt;

&lt;p&gt;To solve this, we defined strict qualification parameters for acceptable lead routing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Homeowner ratio (&lt;code&gt;percentageHomeowners&lt;/code&gt;)&lt;/strong&gt;: Minimum of 50%. Homeowners make long-term property decisions; renters rarely purchase roof replacements or HVAC upgrades.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community scale (&lt;code&gt;residentsCount&lt;/code&gt;)&lt;/strong&gt;: Minimum of 1,000 residents to guarantee adequate market volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Income indicator (&lt;code&gt;averageIncome&lt;/code&gt;)&lt;/strong&gt;: Must meet or exceed $40k to ensure purchasing power for home improvement projects.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Writing the mid-run evaluation logic
&lt;/h3&gt;

&lt;p&gt;We implemented a custom evaluation function in JavaScript that processes each dataset record emitted by Nextdoor Scraper. The script parses formatted strings (such as &lt;code&gt;"56%"&lt;/code&gt; or &lt;code&gt;"$48k"&lt;/code&gt;) into clean numerical values and tests them against our threshold rules.&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="cm"&gt;/**
 * Evaluates whether an extracted Nextdoor dataset item qualifies as a high-intent local lead.
 * @param {Object} item - Structured record from Nextdoor Scraper.
 * @returns {Object} Qualification result containing boolean status and parsed metrics.
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;evaluateLeadQualification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Extract numerical value from percentage string e.g., "56%" -&amp;gt; 56&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;homeownerRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;percentageHomeowners&lt;/span&gt; 
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;percentageHomeowners&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;%&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="p"&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="c1"&gt;// Extract numerical value from income string e.g., "$48k" -&amp;gt; 48000&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;incomeValue&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;averageIncome&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;rawIncome&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;averageIncome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;$&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="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;rawIncome&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="s1"&gt;k&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="nx"&gt;incomeValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawIncome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k&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="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;incomeValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawIncome&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;residentCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;residentsCount&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="c1"&gt;// Qualification thresholds&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MIN_HOMEOWNER_PERCENT&lt;/span&gt; &lt;span class="o"&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;const&lt;/span&gt; &lt;span class="nx"&gt;MIN_INCOME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40000&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;MIN_RESIDENTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&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;isQualified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
        &lt;span class="nx"&gt;homeownerRate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MIN_HOMEOWNER_PERCENT&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;incomeValue&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MIN_INCOME&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;residentCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MIN_RESIDENTS&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="nx"&gt;isQualified&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;homeownerRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;incomeValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;residentCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;affordabilityScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;affordabilityScore&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="na"&gt;friendlinessScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;friendlinessScore&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="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;By embedding this evaluation logic directly into our execution pipeline, every crawled page is screened within milliseconds of extraction. If &lt;code&gt;isQualified&lt;/code&gt; evaluates to &lt;code&gt;true&lt;/code&gt;, the pipeline proceeds to dispatch a real-time Slack notification.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Step 3 (Path A): Sending a Slack notification with no code
&lt;/h2&gt;

&lt;p&gt;Apify Console includes a built-in Slack notification integration for every Actor. It requires no custom code, no webhook endpoint, and no token management. You set it up once directly on the Nextdoor Scraper Actor page and it fires automatically after every run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the Slack notification in Apify Console
&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%2Fmzarzbgkepufvx6hjv7t.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%2Fmzarzbgkepufvx6hjv7t.png" alt="Setting up the Slack notification in Apify Console" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://apify.com/eunit/nextdoor-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; in Apify Console and click the &lt;strong&gt;Integrations&lt;/strong&gt; tab in the Actor's top navigation.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Search integrations&lt;/strong&gt; bar, type &lt;code&gt;slack&lt;/code&gt;. You will see a &lt;strong&gt;Send notification via Slack&lt;/strong&gt; option listed under &lt;strong&gt;Generic integrations&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Send notification via Slack&lt;/strong&gt;. A panel appears with a &lt;strong&gt;Connect workspace&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Connect workspace&lt;/strong&gt; and authorize the Apify app to access your Slack workspace. Select the target channel, such as &lt;code&gt;#local-leads&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Configure when the notification fires. The most useful trigger is &lt;strong&gt;Run succeeded&lt;/strong&gt;, which posts a Slack message automatically each time a Nextdoor Scraper run completes successfully.&lt;/li&gt;
&lt;li&gt;Save the integration. From this point forward, every completed run posts a notification to your chosen Slack channel without any additional code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The notification includes the run status, a direct link to the dataset in Apify Console, and the Actor name. Your sales team gets an immediate signal that fresh neighborhood data is ready to review, without having to log in and check manually.&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%2Fov4mki0yv603wvfpp1at.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%2Fov4mki0yv603wvfpp1at.png" alt="Nextdoor Slack Integration" width="643" height="768"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------------------+      +---------------------+      +------------------------------+
| Nextdoor Scraper   | ---&amp;gt; | Run succeeds        | ---&amp;gt; | Slack: #local-leads          |
| (Apify Console)    |      | (Apify Platform)    |      | "Your run finished. View      |
|                    |      |                     |      |  results in Apify Console."  |
+--------------------+      +---------------------+      +------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the fastest path to getting Slack alerts from Nextdoor Scraper. It takes under two minutes to configure and works on every run automatically, including scheduled runs and saved tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Step 4 (Path B): Using the Apify MCP server with an AI agent
&lt;/h2&gt;

&lt;p&gt;Path A sends a basic completion notification. Path B goes further: it lets an AI agent such as Claude or Cursor actually run Nextdoor Scraper, read and filter the extracted dataset, and post a formatted, actionable Slack alert with specific neighborhood metrics - all from a single conversational prompt.&lt;/p&gt;

&lt;p&gt;This is possible because Apify hosts a remote &lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro" rel="noopener noreferrer"&gt;Model Context Protocol (MCP)&lt;/a&gt; server at &lt;code&gt;https://mcp.apify.com&lt;/code&gt;. Once you connect an MCP client to that server, your AI agent gains access to tools such as &lt;code&gt;call-actor&lt;/code&gt;, &lt;code&gt;search-actors&lt;/code&gt;, and &lt;code&gt;fetch-actor-details&lt;/code&gt;. It can run Nextdoor Scraper on demand and process the results directly inside the chat context.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the Apify MCP server?
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://docs.apify.com/platform/integrations/mcp" rel="noopener noreferrer"&gt;Apify MCP server&lt;/a&gt; exposes Apify Actors as callable tools to external AI clients. It is a programmatic interface that works with any MCP-compatible client, including Claude for Desktop, Cursor, VS Code with MCP support, and others.&lt;/p&gt;

&lt;p&gt;This is distinct from MCP connectors, which work in the opposite direction (an Actor calling an external service). The Apify MCP server makes Actors available as tools your AI agent can invoke from outside Apify entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting the Apify MCP server to your AI client
&lt;/h3&gt;

&lt;p&gt;You authorize once via OAuth browser flow. After that, your AI client can call Nextdoor Scraper directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; - create or update &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in your project:&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;"mcpServers"&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;"apify"&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://mcp.apify.com/?tools=actors,docs,eunit/nextdoor-scraper"&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;&lt;strong&gt;VS Code&lt;/strong&gt; - add to &lt;code&gt;.vscode/mcp.json&lt;/code&gt; or run &lt;strong&gt;MCP: Add Server&lt;/strong&gt; from the Command Palette:&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;"servers"&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;"apify"&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;"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;"http"&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://mcp.apify.com/?tools=actors,docs,eunit/nextdoor-scraper"&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;&lt;strong&gt;Claude for Desktop&lt;/strong&gt; - edit &lt;code&gt;claude_desktop_config.json&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;"mcpServers"&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;"apify"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&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://mcp.apify.com/?tools=actors,docs,eunit/nextdoor-scraper"&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="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 &lt;code&gt;?tools=actors,docs,eunit/nextdoor-scraper&lt;/code&gt; query parameter scopes the tools list to the Nextdoor Scraper Actor specifically, alongside general Actor search and documentation tools. On first connection, a browser OAuth flow opens so you sign in to your Apify account and approve access. No API token is pasted into the chat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the pipeline from a prompt
&lt;/h3&gt;

&lt;p&gt;Once configured, the entire extraction and filtering workflow becomes a single AI prompt. Here is an example of what you would type in Claude or Cursor:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Use Apify to run the Nextdoor Scraper on these four city pages: north-miami--fl, bellaire--oh, bellbrook--oh, and belle-center--oh. Once finished, read the dataset results. Filter for any city where percentageHomeowners is 50% or above and averageIncome is $40k or above. For each qualifying city, format a Slack message with the city name, homeowner share, average income, resident count, and affordability score, then post it to #local-leads.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI agent handles the full sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calls &lt;code&gt;call-actor&lt;/code&gt; with the Nextdoor Scraper Actor ID and input configuration.&lt;/li&gt;
&lt;li&gt;Waits for the run to complete and fetches the dataset items.&lt;/li&gt;
&lt;li&gt;Applies your qualification filter logic across every result.&lt;/li&gt;
&lt;li&gt;Formats a structured message for each qualifying neighborhood.&lt;/li&gt;
&lt;li&gt;Posts the alert to Slack using either a connected Slack MCP tool or a Slack bot token you provide in the chat context.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+------------------+      +----------------------+      +------------------+      +------------------+
| AI Agent         | ---&amp;gt; | Apify MCP server     | ---&amp;gt; | Nextdoor Scraper | ---&amp;gt; | Apify Dataset    |
| (Claude/Cursor)  |      | mcp.apify.com        |      | (Actor run)      |      | (JSON results)   |
| call-actor tool  |      | (OAuth, no token     |      |                  |      |                  |
|                  |      |  in chat)            |      |                  |      |                  |
+------------------+      +----------------------+      +------------------+      +------------------+
         ^                                                                                  |
         |                           reads &amp;amp; filters dataset                               |
         +-----------------------------------------------------------------------------------+
         |
         v
+------------------+
| Slack #local-leads|
| Formatted alert  |
| per qualifying   |
| neighborhood     |
+------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this approach works well for local lead routing
&lt;/h3&gt;

&lt;p&gt;The AI agent approach is especially effective because filtering logic does not need to be written in advance or maintained as code. You adjust the criteria conversationally. If the roofing company decides to lower the homeowner threshold to 45% for a quieter week, you change one line in the prompt. No code deployment required.&lt;/p&gt;

&lt;p&gt;The Apify MCP server also supports &lt;strong&gt;output schema inference&lt;/strong&gt;, meaning Claude or Cursor understands the structure of the Nextdoor Scraper dataset before the run completes. That makes it easier for the agent to write accurate filter logic against fields like &lt;code&gt;percentageHomeowners&lt;/code&gt;, &lt;code&gt;averageIncome&lt;/code&gt;, and &lt;code&gt;residentsCount&lt;/code&gt; without needing to inspect a raw JSON file first.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. What the end result looks like in both paths
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Path A: What the no-code Slack notification looks like
&lt;/h3&gt;

&lt;p&gt;When a Nextdoor Scraper run finishes, the built-in Slack integration posts a notification automatically to your chosen channel. The message includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Actor name and run status&lt;/li&gt;
&lt;li&gt;A direct link to the dataset in Apify Console&lt;/li&gt;
&lt;li&gt;The run duration and the number of items stored&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your sales team opens Slack, sees the notification, and clicks through to the dataset to review results. No CSV downloads, no email chains, no delayed handoffs.&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%2Fhh3ajs23i8hwmnexods6.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%2Fhh3ajs23i8hwmnexods6.png" alt="Nextdoor Scraper Actor Succeeded" width="739" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For teams that run the scraper on a schedule (e.g. every Monday morning before the weekly sales meeting), this turns a manual check-in into a reliable, automated briefing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path B: What the AI agent prompt workflow looks like
&lt;/h3&gt;

&lt;p&gt;With the Apify MCP server connected to Claude or Cursor, the agent runs the full pipeline from one prompt. Here is what a real output message from Claude might look like after processing a Nextdoor Scraper dataset with a 50% homeowner filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--------------------------------------------------------------+
|  #local-leads                                       [Slack]  |
+--------------------------------------------------------------+
|  🏡 High-intent lead: North Miami, FL                        |
|  Homeowner share: 54%  |  Avg income: $48k                   |
|  Residents: 59,854     |  Affordability: 82/100              |
|  Nextdoor page: https://nextdoor.com/city/north-miami--fl/   |
|                                                              |
|  🏡 High-intent lead: Bellbrook, OH                          |
|  Homeowner share: 61%  |  Avg income: $52k                   |
|  Residents: 7,009      |  Affordability: 91/100              |
|  Nextdoor page: https://nextdoor.com/city/bellbrook--oh/     |
+--------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Lake Rock Ventures pilot, switching from manual CSV review to the Apify MCP server + Claude workflow reduced the time from data collection to campaign briefing from &lt;strong&gt;48 hours to under 10 minutes&lt;/strong&gt;, while narrowing outreach to high-ownership areas only.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Wrapping up
&lt;/h2&gt;

&lt;p&gt;Static data collection is no longer sufficient for modern local marketing and lead generation operations. By replacing manual CSV exports with an automated pipeline powered by &lt;a href="https://apify.com/eunit/nextdoor-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; and Apify MCP connectors, you can bridge the gap between web data extraction and real-time sales execution.&lt;/p&gt;

&lt;p&gt;With this architecture, you benefit from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hyper-local precision&lt;/strong&gt;: Extracting structured resident counts, homeowner ratios, and income signals from public Nextdoor pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictable cost scaling&lt;/strong&gt;: Leveraging Pay-Per-Event pricing to pay strictly for successful record extractions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero glue-code overhead&lt;/strong&gt;: Eliminating third-party integration tools and AWS Lambda scripts by using Apify MCP connectors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant lead velocity&lt;/strong&gt;: Routing high-intent local opportunities straight to your sales team's Slack channel in seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ready to automate your local lead routing? Try &lt;a href="https://apify.com/eunit/nextdoor-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; in the Apify Store, connect your Slack workspace via Apify MCP connectors, and start turning public neighborhood signals into real-time sales momentum today.&lt;/p&gt;

</description>
      <category>nexdoorscraping</category>
      <category>apify</category>
      <category>agents</category>
      <category>salesautomation</category>
    </item>
    <item>
      <title>How to download public YouTube videos at scale with an Apify Actor</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Thu, 18 Jun 2026 12:30:13 +0000</pubDate>
      <link>https://dev.to/eunit/how-to-download-public-youtube-videos-at-scale-with-an-apify-actor-4i7b</link>
      <guid>https://dev.to/eunit/how-to-download-public-youtube-videos-at-scale-with-an-apify-actor-4i7b</guid>
      <description>&lt;p&gt;You open a YouTube page. The video is public. You need that video - or its transcript, or its metadata, or just a clean download link you can pass to the next step in your workflow.&lt;/p&gt;

&lt;p&gt;So you search for a YouTube video downloader. You find a browser extension, a sketchy website, maybe a command-line tool. You paste the URL. It works - once. But you have twenty more URLs to process. Or you need to run this same task every Monday morning. Or three people on your team need to do the same thing without using three different methods and getting three different outputs.&lt;/p&gt;

&lt;p&gt;That is where every basic downloader starts to fall apart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/youtube-video-downloader" rel="noopener noreferrer"&gt;YouTube Video Downloader&lt;/a&gt; is an Apify Actor built to solve exactly this problem. It processes public YouTube watch URLs, YouTube Shorts, and live replay links in a structured, repeatable, automatable way. You get download format URLs, video metadata, thumbnails, channel data, and optional transcript text - all in one clean dataset output per run.&lt;/p&gt;

&lt;p&gt;This guide will show you exactly how it works, how to configure it, and why it is a better fit than generic one-off tools if your workflow involves more than one video or one person.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the YouTube Video Downloader Actor does
&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%2Ff74t6wemgoih8ztjdzk6.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%2Ff74t6wemgoih8ztjdzk6.png" alt="Youtube Video downloader" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most downloader tools treat the task as simple: give them a URL and they give you a file. That is fine if that is truly all you need.&lt;/p&gt;

&lt;p&gt;But most teams need a bit more. They need the title and description to track what they downloaded. They need the thumbnail to display in a content board. They need the transcript to feed into a summarization or search tool. And they need all of this in a format that works with the rest of their stack - not a browser download that lands in someone's desktop folder.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/youtube-video-downloader" rel="noopener noreferrer"&gt;YouTube Video Downloader&lt;/a&gt; handles all of that in one run. Here is what it can return for each URL you give it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Direct download URLs for available MP4 and WebM formats&lt;/li&gt;
&lt;li&gt;Progressive (video + audio combined) and adaptive (video-only or audio-only) stream metadata&lt;/li&gt;
&lt;li&gt;Video title, description, tags, and view count&lt;/li&gt;
&lt;li&gt;Channel name and channel ID&lt;/li&gt;
&lt;li&gt;Duration in seconds&lt;/li&gt;
&lt;li&gt;Thumbnail URL (including the high-resolution version when available)&lt;/li&gt;
&lt;li&gt;Media type - regular video, Short, or live replay&lt;/li&gt;
&lt;li&gt;Public transcript text when available and enabled&lt;/li&gt;
&lt;li&gt;A saved file in the run's key-value store when you use file export mode&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Actor runs on the Apify platform, which means you are not limited to a single manual session. You can run it from Apify Console, call it via API from your own application, schedule it on a recurring basis, or trigger it with a webhook. The output lands in a structured dataset you can export in JSON, CSV, or Excel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who this Actor is actually useful for
&lt;/h2&gt;

&lt;p&gt;Before you decide whether this tool is worth using, it helps to know whether your situation matches how it was designed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content and editorial teams&lt;/strong&gt; are a natural fit. If your team regularly works with public video assets - for clipping, repurposing, archiving, or editorial reference - you likely need more than the video file itself. You need the metadata alongside it. The Actor gives you both in a consistent format, run after run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agencies and operations teams&lt;/strong&gt; benefit most from the repeatability. When different people handle different requests manually, the output is inconsistent. One person uses one site, another uses a different tool. The Actor standardizes the intake: one input format, one output structure, every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developers and product teams&lt;/strong&gt; often want a YouTube video downloader API - something they can call from inside their application or automation, not something they have to operate manually. The Actor exposes a full REST API and is also available through the JavaScript and Python client libraries. You can integrate it into any stack in a few lines of code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Researchers and analysts&lt;/strong&gt; who work with transcripts, topic metadata, or structured video data will find the Actor useful for building consistent datasets from public YouTube content. Instead of scraping each video manually, you can pass a list of URLs and get back a single structured export.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why browser-based downloaders are not enough
&lt;/h2&gt;

&lt;p&gt;It is worth being direct about this, because plenty of people reading this article have already tried the obvious alternatives.&lt;/p&gt;

&lt;p&gt;Browser-based YouTube downloader sites work for one-off tasks. You paste a link, download a file, and you are done. But they are built for individual, manual use. They do not give you metadata. They do not store transcripts. They do not support API access. They do not let you schedule a run or trigger one programmatically. And they definitely do not output a structured dataset you can hand to the next step in a pipeline.&lt;/p&gt;

&lt;p&gt;There is also the reliability issue. Many of these sites change frequently, break without warning, or quietly stop working after YouTube updates. If you build a team process around one of them, you are building on unstable ground.&lt;/p&gt;

&lt;p&gt;The YouTube Video Downloader Actor approaches the problem differently. Direct format URLs returned by YouTube are time-limited - they expire. The Actor solves this by giving you two options: use the expiring URL for immediate access, or switch to &lt;code&gt;save-best-progressive&lt;/code&gt; mode and have the Actor store a durable copy in Apify's key-value store. From there, you can fetch it via a stable URL for as long as you need it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-step: how to download public YouTube videos with Apify
&lt;/h2&gt;

&lt;p&gt;Here is exactly how to run the Actor and get results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Open the Actor in Apify Console
&lt;/h3&gt;

&lt;p&gt;Go to the &lt;a href="https://apify.com/eunit/youtube-video-downloader" rel="noopener noreferrer"&gt;YouTube Video Downloader&lt;/a&gt; page in Apify Store. If you do not have an Apify account yet, you can sign up for free - no credit card required to start.&lt;/p&gt;

&lt;p&gt;Once you are in, click &lt;strong&gt;Try for free&lt;/strong&gt; to open the Actor in Apify Console. You will land on the &lt;strong&gt;Input&lt;/strong&gt; tab, which is where you configure everything before running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Add your YouTube URLs
&lt;/h3&gt;

&lt;p&gt;In the &lt;strong&gt;YouTube video URLs&lt;/strong&gt; field, paste one or more public YouTube links. The Actor supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Standard watch links: &lt;code&gt;https://www.youtube.com/watch?v=...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;YouTube Shorts: &lt;code&gt;https://www.youtube.com/shorts/...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Live replay links for completed live streams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can add up to 100 URLs per run (controlled by the &lt;strong&gt;Max videos&lt;/strong&gt; input field, which defaults to 10). If you have a longer list, you can run multiple jobs or call the Actor programmatically in batches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Choose your download mode
&lt;/h3&gt;

&lt;p&gt;This is where the Actor becomes more powerful than a generic downloader. Under &lt;strong&gt;Download mode&lt;/strong&gt;, you have two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;metadata-only&lt;/strong&gt; - The Actor returns format URLs, metadata, and optional transcript data. No file is saved. This is the default mode and the fastest way to get structured output. Use this when you only need the download link or metadata to pass downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;save-best-progressive&lt;/strong&gt; - The Actor selects the best available progressive format (video + audio combined), downloads it, and stores the file in the run's default key-value store. You can then fetch it via a stable Apify URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set your preferred quality (&lt;code&gt;best&lt;/code&gt;, &lt;code&gt;720p&lt;/code&gt;, &lt;code&gt;1080p&lt;/code&gt;, etc.) and container (&lt;code&gt;mp4&lt;/code&gt;, &lt;code&gt;webm&lt;/code&gt;, or &lt;code&gt;any&lt;/code&gt;) to guide which format gets selected when saving.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Configure the rest of your input
&lt;/h3&gt;

&lt;p&gt;A few more fields worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Include transcript&lt;/strong&gt; - Toggle this on if you need the public transcript text. When available, it gets included in the dataset output for each video. Useful for search indexing, summarization, and AI enrichment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include progressive formats&lt;/strong&gt; - Returns combined video + audio formats in the output. On by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include adaptive formats&lt;/strong&gt; - Returns video-only and audio-only streams separately. On by default. Useful if you are working with adaptive streaming workflows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max formats per video&lt;/strong&gt; - Limits how many format entries appear in the output. Default is 8. Increase this if you want to see the full list of available streams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Max saved file size (MB)&lt;/strong&gt; - A safety cap when using &lt;code&gt;save-best-progressive&lt;/code&gt; mode. Defaults to 150 MB. Increase if you are working with longer videos and have confirmed the file will fit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is an example input that covers most use cases:&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;"startUrls"&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://www.youtube.com/watch?v=dQw4w9WgXcQ"&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://www.youtube.com/shorts/abc123"&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;"maxVideos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preferredQuality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"720p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"preferredContainer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeProgressiveFormats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeAdaptiveFormats"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"downloadMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"metadata-only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeTranscript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxDownloadSizeMb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150&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;h3&gt;
  
  
  Step 5: Start the run
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Start&lt;/strong&gt; in Apify Console. The Actor processes each URL in sequence, resolves the available formats, collects the metadata, and pushes one item to the dataset for each URL it handles.&lt;/p&gt;

&lt;p&gt;You do not need to keep the browser open. Apify Console shows run status in real time, and you can come back when it is done. For large batches or scheduled runs, you will not be watching at all - the output will just be there when you need it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Review and use the output
&lt;/h3&gt;

&lt;p&gt;Once the run finishes, open the &lt;strong&gt;Dataset&lt;/strong&gt; tab. You will see one row per input URL. Each row has:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it gives you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ready&lt;/code&gt;, &lt;code&gt;downloaded&lt;/code&gt;, or &lt;code&gt;error&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;videoId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The normalized YouTube video ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The video title from YouTube&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mediaType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Video, Short, or live replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;durationSeconds&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runtime in seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;viewCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Public view count at the time of the run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;thumbnailUrl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;High-resolution thumbnail URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;formats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All returned format entries with stream URLs and quality labels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;transcript&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Public transcript text (if enabled and available)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;selectedDownload&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key-value store key and public URL of saved file (if applicable)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can export the dataset from the &lt;strong&gt;Export&lt;/strong&gt; button as JSON, CSV, or Excel. If you saved files using &lt;code&gt;save-best-progressive&lt;/code&gt; mode, they appear in the &lt;strong&gt;Storage&lt;/strong&gt; tab under the run's key-value store.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to integrate the Actor into your own code
&lt;/h2&gt;

&lt;p&gt;If you want to call the Actor programmatically - for example, from an internal tool, a script, or a larger automation - the easiest way is through the official Apify client libraries.&lt;/p&gt;

&lt;h3&gt;
  
  
  JavaScript / TypeScript
&lt;/h3&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;ApifyClient&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;apify-client&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;client&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;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_API_TOKEN&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;run&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eunit/youtube-video-downloader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;startUrls&lt;/span&gt;&lt;span class="p"&gt;:&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="s1"&gt;https://www.youtube.com/watch?v=dQw4w9WgXcQ&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;downloadMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;metadata-only&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;includeTranscript&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;preferredQuality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;720p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;listItems&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;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Python
&lt;/h3&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;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;run&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;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eunit/youtube-video-downloader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&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;startUrls&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.youtube.com/watch?v=dQw4w9WgXcQ&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;downloadMode&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;metadata-only&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;includeTranscript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preferredQuality&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;720p&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the client with &lt;code&gt;pip install apify-client&lt;/code&gt; (Python) or &lt;code&gt;npm install apify-client&lt;/code&gt; (JavaScript/TypeScript). Your API token is in Apify Console under &lt;strong&gt;Settings &amp;gt; Integrations&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A note on legal and responsible use
&lt;/h2&gt;

&lt;p&gt;Any good guide on YouTube video downloading should address this clearly.&lt;/p&gt;

&lt;p&gt;The YouTube Video Downloader Actor is built for &lt;strong&gt;public YouTube content&lt;/strong&gt; - videos that are publicly visible without login. That does not automatically mean every public video is fair game to download, archive, or redistribute.&lt;/p&gt;

&lt;p&gt;YouTube's terms of service generally prohibit downloading videos without explicit permission from the platform or the content owner. There are exceptions for content you own, content licensed for download, and content in the public domain - but those are the exceptions, not the default.&lt;/p&gt;

&lt;p&gt;If you are asking &lt;strong&gt;how to download YouTube videos legally&lt;/strong&gt;, the clearest answer is: work with content you own, content you have been licensed to use, or content in the public domain. If you are unsure about a specific piece of content, check with a qualified legal advisor, not a downloader tool's FAQ.&lt;/p&gt;

&lt;p&gt;For teams, this matters even more. A repeatable workflow that processes video at scale creates a larger footprint. Good compliance means having clear answers to: what content are we allowed to process, where does the output go, and who approved it.&lt;/p&gt;

&lt;p&gt;The Actor helps you build a consistent, auditable workflow. The permissions question is something you need to answer before you run it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common use cases where the Actor earns its cost
&lt;/h2&gt;

&lt;p&gt;If you are comparing this Actor against free browser-based tools, the question is not whether the free tool works for one video. It usually does. The question is what your workflow actually costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content repurposing pipelines.&lt;/strong&gt; If your team regularly turns public video into clips, summaries, or knowledge-base articles, you need the transcript and metadata alongside the media file. The Actor gives you all three in one run, in a structured format that fits your next tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approved asset management.&lt;/strong&gt; Agencies and media teams that manage approved public video assets across campaigns or clients need consistency. The Actor standardizes the output, so every URL processed the same way, by anyone on the team, gives the same fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI and research workflows.&lt;/strong&gt; Transcripts combined with metadata - title, tags, description, view count, duration - are a useful input for classification, topic modeling, or AI summarization pipelines. The Actor gives you a clean starting point for that kind of enrichment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product integration.&lt;/strong&gt; If you are building an internal tool or product feature that involves YouTube video data, the Actor is a faster path than building and maintaining your own downloader infrastructure. You call an API, you get structured output, and you handle the hard part (your product logic) rather than YouTube format resolution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why automation changes the value equation
&lt;/h2&gt;

&lt;p&gt;Here is a way to think about whether the Actor is worth paying for.&lt;/p&gt;

&lt;p&gt;If you download five YouTube videos a month manually, the cost of manual work is low. A free tool is probably fine.&lt;/p&gt;

&lt;p&gt;If you process fifty URLs every week across a team of three people, the math changes. Manual work is time, and time is money. If each URL takes five minutes to process by hand (finding the right tool, pasting the link, downloading the file, renaming it, noting the metadata, passing it to the next step), that is over four hours of manual work per week. At scale, or across a team, that adds up fast.&lt;/p&gt;

&lt;p&gt;The real cost of manual tools is not the tool's price. It is the time spent, the inconsistency in output, and the fragility when the tool breaks or the website changes.&lt;/p&gt;

&lt;p&gt;For teams that process video data regularly, the automation pays for itself quickly. And for teams that need scheduled or API-triggered runs - the kind of workflow that simply cannot be done with a manual tool - the comparison is not really about price at all.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Can this Actor download YouTube Shorts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The Actor supports public YouTube Shorts URLs. They are handled as a supported media type and included in the dataset output with the &lt;code&gt;mediaType&lt;/code&gt; field set to &lt;code&gt;Short&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it return direct download links?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. When available, the Actor returns direct format URLs in the &lt;code&gt;formats&lt;/code&gt; array. These URLs are time-limited, so use them promptly or switch to &lt;code&gt;save-best-progressive&lt;/code&gt; mode to get a durable stored copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it save the video file, not just the URL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Set &lt;code&gt;downloadMode&lt;/code&gt; to &lt;code&gt;save-best-progressive&lt;/code&gt; and the Actor will select the best available progressive format, download it, and store it in the run's key-value store. You can then access it via a stable public URL from the &lt;code&gt;selectedDownload&lt;/code&gt; field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it extract transcripts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, when the video has a public transcript and you enable the &lt;code&gt;includeTranscript&lt;/code&gt; option. Not all videos have public transcripts, and in those cases the Actor returns a structured item without transcript data rather than failing the run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work for live replays?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Completed live streams that are publicly available on YouTube are supported as a media type. The Actor handles them the same way as regular videos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run this on a schedule or trigger it with a webhook?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Scheduled runs and webhook triggers are both available from Apify Console. You can also call the Actor via the REST API or the official client libraries to integrate it into any external system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why use this instead of a free downloader?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because free downloaders are built for one-off, manual, single-user tasks. They do not return structured metadata. They do not support transcripts. They do not give you API access, scheduled runs, or structured exports. And they break without warning. If your workflow involves more than one video, more than one person, or more than a one-time task, a structured Actor on a stable platform is a significantly better fit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;If you have a workflow that involves downloading public YouTube videos - or extracting metadata, transcripts, thumbnails, or stream format data from them - &lt;a href="https://apify.com/eunit/youtube-video-downloader" rel="noopener noreferrer"&gt;YouTube Video Downloader&lt;/a&gt; is built for exactly that.&lt;/p&gt;

&lt;p&gt;You can start with a single public URL, review the dataset output, and see in one run how much cleaner and more useful the output is compared to what you get from a browser-based tool.&lt;/p&gt;

&lt;p&gt;From there, you can scale up to batches, schedule recurring runs, or connect it to your own code using the Apify JavaScript or Python clients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://apify.com/eunit/youtube-video-downloader" rel="noopener noreferrer"&gt;Open YouTube Video Downloader on Apify →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>apify</category>
      <category>youtube</category>
      <category>download</category>
    </item>
    <item>
      <title>How to download Instagram videos and Reels at scale with Apify</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Wed, 17 Jun 2026 11:27:09 +0000</pubDate>
      <link>https://dev.to/eunit/how-to-download-instagram-videos-and-reels-at-scale-with-apify-2ilg</link>
      <guid>https://dev.to/eunit/how-to-download-instagram-videos-and-reels-at-scale-with-apify-2ilg</guid>
      <description>&lt;p&gt;Picture this. Your marketing team just wrapped a three-month creator campaign. Thirty public Reels, a handful of IGTV videos, and dozens of short posts - all published, all public, and all living exclusively on Instagram. Now someone asks: "Can we archive these for the brand library?"&lt;/p&gt;

&lt;p&gt;You open your browser, paste a URL into a random download site, and... nothing. Or worse, you get an ad-laden page with a broken download button. You try four more sites. Two work. One gives you a 240p file. And you've just spent twenty minutes manually processing what should take seconds.&lt;/p&gt;

&lt;p&gt;Sound familiar? This is the reality for anyone managing Instagram video content at even a modest scale. The good news is there's a better way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; is an Apify Actor built specifically for this. It handles public Instagram Reels, &lt;code&gt;/p/&lt;/code&gt; video posts, and IGTV (&lt;code&gt;/tv/&lt;/code&gt;) links in a repeatable, automation-ready workflow - giving you direct video URLs, metadata, thumbnails, and optional stored files, all in clean structured output.&lt;/p&gt;

&lt;p&gt;This article walks you through exactly how it works, who it's for, and how to go from zero to a working download workflow in under ten minutes.&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.amazonaws.com%2Fuploads%2Farticles%2Furjd6jh8l91es82p6f8e.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.amazonaws.com%2Fuploads%2Farticles%2Furjd6jh8l91es82p6f8e.png" alt="Instagram Video and Reel Downloader" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why downloading Instagram videos is harder than it looks
&lt;/h2&gt;

&lt;p&gt;On the surface, saving an Instagram video sounds trivial. The content is public. The URL is right there. How hard can it be?&lt;/p&gt;

&lt;p&gt;Harder than expected, it turns out.&lt;/p&gt;

&lt;p&gt;Instagram doesn't expose direct video file URLs through any official public API. The platform serves video content dynamically from its content delivery network, and the URLs it generates are session-bound, expire quickly, and are deeply embedded in the page's internal data structures. What you see in your browser isn't what a simple HTTP request sees.&lt;/p&gt;

&lt;p&gt;On top of that, Reels in particular behave differently from regular posts. Some pages expose enough data in the static HTML for a fast scraper to pick up the video URL immediately. Others don't - they require a realistic browser session to load the required JavaScript and resolve the actual media delivery request before the video URL becomes visible.&lt;/p&gt;

&lt;p&gt;This is why so many free Instagram downloader websites are inconsistent. They work sometimes, fail silently other times, and give you no real explanation when something goes wrong. For a one-off personal download, that's annoying but acceptable. For a business workflow, it's a real problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; was designed around this reality. It uses a two-path extraction approach: try the fast static route first, and fall back to a browser-based session for harder Reel pages that don't expose the video URL in their initial HTML. The result is a more dependable tool for real public content workflows.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Instagram Video and Reel Downloader actually does
&lt;/h2&gt;

&lt;p&gt;Let's be clear about what this Actor is and isn't.&lt;/p&gt;

&lt;p&gt;It is not a browser extension, not a mobile app, and not a one-click website. It's an &lt;a href="https://apify.com/store?fpr=eunit" rel="noopener noreferrer"&gt;Apify Actor&lt;/a&gt; - a serverless cloud program that accepts structured input, performs a task, and returns structured output. You run it from &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify Console&lt;/a&gt;, call it through the API, or trigger it from an automation. It keeps a record of every run, and you can reuse the same configuration whenever you need it.&lt;/p&gt;

&lt;p&gt;More specifically, for each public Instagram URL you give it, the Actor can return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a direct &lt;code&gt;videoUrl&lt;/code&gt; pointing to the resolved video file&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;thumbnailUrl&lt;/code&gt; for the post's preview image&lt;/li&gt;
&lt;li&gt;the post &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; when available&lt;/li&gt;
&lt;li&gt;the canonical URL, &lt;code&gt;shortcode&lt;/code&gt;, and &lt;code&gt;mediaType&lt;/code&gt; (&lt;code&gt;reel&lt;/code&gt;, &lt;code&gt;post&lt;/code&gt;, or &lt;code&gt;tv&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;optional file references if you choose to store the actual video or image in Apify's key-value store&lt;/li&gt;
&lt;li&gt;structured error data (with an &lt;code&gt;errorCode&lt;/code&gt; and &lt;code&gt;errorMessage&lt;/code&gt;) if a URL can't be resolved - so the run stays auditable even when something goes wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters more than it might seem. Most free downloader tools just... fail. No error message, no record, no way to know what happened. This Actor keeps a dataset item for every URL it processes, whether it succeeded or not. For teams tracking a batch of URLs, that's a meaningful difference.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ready to try it?&lt;/strong&gt; Open &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; in Apify Console and run it directly in your browser - no code required.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Who actually needs this?
&lt;/h2&gt;

&lt;p&gt;Before getting into the walkthrough, it's worth being honest about who this tool is and isn't for.&lt;/p&gt;

&lt;p&gt;If you occasionally want to save a single Instagram Reel for personal use, a free browser extension or a consumer downloader site will probably do the job. This Actor isn't aimed at that use case.&lt;/p&gt;

&lt;p&gt;Where it earns its place is in repeatable, team-level, or integration-heavy workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Social media managers and content teams&lt;/strong&gt; often need downloadable copies of published Reels - for brand libraries, cross-platform repurposing, or simply keeping a backup of content that lives only on Instagram. Running a batch of URLs through this Actor takes minutes instead of an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agencies&lt;/strong&gt; working with multiple clients or creator rosters regularly need to pull public video assets into approval workflows, reporting decks, or content management systems. A structured JSON output with direct video URLs and thumbnails is far easier to feed into those systems than manually downloading files and uploading them elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Developers and automation builders&lt;/strong&gt; can use the Actor as a building block. The Apify platform gives you API access, webhooks, and scheduling - so a workflow that starts with "download these public Instagram videos" can chain directly into "save to Google Drive," "send for review," or "push metadata to a spreadsheet."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Researchers and operations teams&lt;/strong&gt; sometimes need video URLs and metadata for tracking or reporting purposes - not necessarily the files themselves. The dataset output covers that without requiring extra storage overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-step: how to use the Actor from Apify Console
&lt;/h2&gt;

&lt;p&gt;You don't need to write a single line of code to get your first result. Here's the full walkthrough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Open the Actor
&lt;/h3&gt;

&lt;p&gt;Go to &lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;apify.com/eunit/instagram-video-and-reel-downloader&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you don't already have an Apify account, you can create one for free. Once you're in, you'll land on the Actor's overview page. Read through the README - it's short and covers the key things to know before you run anything. Then click the &lt;strong&gt;Input&lt;/strong&gt; tab.&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.amazonaws.com%2Fuploads%2Farticles%2F996ppnzixmdblavna3cq.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.amazonaws.com%2Fuploads%2Farticles%2F996ppnzixmdblavna3cq.png" alt="Instagram Video and Reel Downloader on Apify" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Collect your public Instagram URLs
&lt;/h3&gt;

&lt;p&gt;The Actor supports three URL formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/reel/&lt;/code&gt; - standard Instagram Reels&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/p/&lt;/code&gt; - regular photo and video posts&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/tv/&lt;/code&gt; - IGTV-style long-form video&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy the public URLs you want to process. The critical word there is &lt;em&gt;public&lt;/em&gt;. This tool doesn't attempt to access private accounts, private posts, or restricted content. If the post isn't publicly viewable without logging in, the Actor will return an error item for that URL rather than silently pretending it worked.&lt;/p&gt;

&lt;p&gt;A quick way to check: open the URL in a private/incognito browser window. If you can see the video without logging in, the Actor can process it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure the input
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Input&lt;/strong&gt; tab in Apify Console gives you a form to fill in. Here's what each field does:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;startUrls&lt;/code&gt;&lt;/strong&gt; - This is the only required field. It's the list of Instagram URLs you want the Actor to process. You can add one or fifty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;storeVideoFiles&lt;/code&gt;&lt;/strong&gt; - When this is enabled, the Actor doesn't just resolve the video URL - it also downloads the actual video file and stores it in Apify's key-value store, accessible via a public URL. Useful when you need the binary file, not just a link. Leave it off if the &lt;code&gt;videoUrl&lt;/code&gt; in the dataset is sufficient for your workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;storeThumbnailFiles&lt;/code&gt;&lt;/strong&gt; - Same idea, but for the post's thumbnail image. Enable it if you need the actual image file stored alongside the video reference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;includePageHtml&lt;/code&gt;&lt;/strong&gt; - Saves the raw Instagram HTML from each fetched page to key-value store. You'll rarely need this, but it's a useful debugging tool if Instagram changes its page structure and the Actor stops resolving video URLs correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;filenamePrefix&lt;/code&gt;&lt;/strong&gt; - If you're storing files, this prefix gets prepended to every stored filename. Something like &lt;code&gt;campaign-june-2026&lt;/code&gt; makes it easy to find related files later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;maxConcurrency&lt;/code&gt;&lt;/strong&gt; - Controls how many pages the Actor processes in parallel. The default of 3 is a reasonable starting point. Keep this lower (1 or 2) if Instagram is rate-limiting your requests; only push it higher if speed is a priority and you're willing to accept some request failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;maxRequestRetries&lt;/code&gt;&lt;/strong&gt; - How many times the Actor retries a failed page request before marking it as an error. The default of 2 is fine for most use cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requestTimeoutSecs&lt;/code&gt;&lt;/strong&gt; - The timeout for individual page fetches and optional file downloads. 30 seconds works well under normal conditions; increase it if you're on a slow connection or downloading large video files into storage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;proxyConfiguration&lt;/code&gt;&lt;/strong&gt; - Optional proxy settings. Most users don't need this to start. If you run into consistent request failures or geo-blocking behavior, enabling Apify Proxy here can help distribute requests more effectively.&lt;/p&gt;

&lt;p&gt;A realistic input configuration for a small batch might look 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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"startUrls"&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://www.instagram.com/reel/ABC123xyz/"&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://www.instagram.com/p/DEF456uvw/"&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://www.instagram.com/reel/GHI789rst/"&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;"storeVideoFiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"storeThumbnailFiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includePageHtml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"filenamePrefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"campaign-june-2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&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;"maxRequestRetries"&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;"requestTimeoutSecs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proxyConfiguration"&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;"useApifyProxy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frd6wgovr8hnu4rk03jel.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.amazonaws.com%2Fuploads%2Farticles%2Frd6wgovr8hnu4rk03jel.png" alt="Sample Input on Apify Console" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Run the Actor
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;Run&lt;/strong&gt; in the top-right corner.&lt;/p&gt;

&lt;p&gt;The Actor starts processing your URLs immediately. For straightforward public posts and video pages, it extracts the video URL and metadata from the static page response - fast, efficient, no browser overhead. For trickier Reel pages that don't expose the video delivery URL in their initial HTML, it falls back to a browser-based path that can capture the actual media requests.&lt;/p&gt;

&lt;p&gt;You don't need to manage any of this yourself. The Actor handles the routing internally.&lt;/p&gt;

&lt;p&gt;You can watch the logs in real time as the Actor runs. Each URL gets processed, and any failures surface immediately with an error code and message rather than a silent skip.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Review the dataset output
&lt;/h3&gt;

&lt;p&gt;Once the run completes, click the &lt;strong&gt;Dataset&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;Each URL you submitted produces one dataset item. A successful result looks 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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ready"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputUrl"&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.instagram.com/reel/ABC123xyz/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canonicalUrl"&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.instagram.com/reel/ABC123xyz/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shortcode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ABC123xyz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mediaType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reel"&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;"Instagram video"&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;"Caption text from the post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"videoUrl"&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://instagram.fcdn.example/v/t16/f1/video.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"thumbnailUrl"&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://instagram.fcdn.example/v/t51/thumb.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;"videoStoreKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"video-campaign-june-2026-abc123xyz.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"videoPublicUrl"&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://api.apify.com/v2/key-value-stores/STORE_ID/records/video-campaign-june-2026-abc123xyz.mp4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uploadDate"&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-05-15T14:30:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fetchedAt"&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-06-17T10:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;A failed result is just as structured:&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputUrl"&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.instagram.com/reel/PRIVATE123/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NO_VIDEO_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errorMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Could not resolve a public video URL from this page."&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;You can export the dataset in JSON, CSV, Excel, or HTML format - whichever fits your downstream workflow. If you enabled file storage, you can access all stored video and image files directly from the run's &lt;strong&gt;Storage&lt;/strong&gt; tab.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Put the output to work
&lt;/h3&gt;

&lt;p&gt;This is where the Actor starts earning its value beyond a simple file download.&lt;/p&gt;

&lt;p&gt;If your workflow is "download these URLs occasionally and review them manually," exporting the dataset as CSV and opening it in a spreadsheet is enough.&lt;/p&gt;

&lt;p&gt;If your workflow is more continuous - weekly campaign archives, automated content pipelines, or team-level review processes - the Apify platform gives you several paths to build on this run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API calls:&lt;/strong&gt; Trigger the Actor from any script using the Apify JavaScript or Python client, or a direct HTTP request. The output is the same structured dataset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schedules:&lt;/strong&gt; Run the Actor on a weekly or daily cadence with a fresh list of URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks:&lt;/strong&gt; Fire a webhook when the run completes to trigger downstream steps in Make, Zapier, n8n, or your own backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP server:&lt;/strong&gt; If you're building AI workflows, the Actor is available as a tool through Apify's Model Context Protocol (MCP) server.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Integrating the Actor with code
&lt;/h2&gt;

&lt;p&gt;If you're a developer or want to trigger the Actor programmatically, the setup is straightforward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JavaScript / TypeScript:&lt;/strong&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;ApifyClient&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;apify-client&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;client&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;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_API_TOKEN&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;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;startUrls&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="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="s1"&gt;https://www.instagram.com/reel/ABC123xyz/&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="na"&gt;storeVideoFiles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;filenamePrefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-archive&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;run&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eunit/instagram-video-and-reel-downloader&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&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;items&lt;/span&gt; &lt;span class="p"&gt;}&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;listItems&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ready&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="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="s1"&gt;Video URL:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;videoUrl&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;&lt;strong&gt;Python:&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;from&lt;/span&gt; &lt;span class="n"&gt;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;run_input&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;startUrls&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.instagram.com/reel/ABC123xyz/&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;storeVideoFiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filenamePrefix&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;my-archive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;run&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;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eunit/instagram-video-and-reel-downloader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_input&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&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;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;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;ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Video 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;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;videoUrl&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;Install the client with &lt;code&gt;npm install apify-client&lt;/code&gt; (JavaScript) or &lt;code&gt;pip install apify-client&lt;/code&gt; (Python), then drop in your Apify API token from Apify Console under &lt;strong&gt;Settings &amp;gt; Integrations&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  When it makes sense to pay for a reliable Actor
&lt;/h2&gt;

&lt;p&gt;Let's address the obvious question: why pay for this when free downloader sites exist?&lt;/p&gt;

&lt;p&gt;The honest answer is that free downloader sites are built for individual, one-off use. They're not designed to process batches, return structured data, integrate with APIs, or recover gracefully when Instagram changes something. They also come with ad loads, pop-ups, and opaque data practices that you probably don't want in a business workflow.&lt;/p&gt;

&lt;p&gt;This Actor uses a pay-per-event model at $90.00 per 1,000 processed videos. That pricing structure means you pay for actual completed work - not a flat subscription that bills you whether you use it or not.&lt;/p&gt;

&lt;p&gt;For context: if a marketing manager spends 30 minutes manually downloading and organizing 10 Instagram videos per week, that's roughly 26 hours of manual work per year. The same batch run through this Actor costs under $1 and completes in a few seconds.&lt;/p&gt;

&lt;p&gt;The economics shift further when you factor in reliability. A workflow that fails halfway through a batch, returns corrupted files, or gives you no error visibility isn't really "free" - it's just deferring the cost into your time and frustration budget.&lt;/p&gt;

&lt;p&gt;Paying customers here are buying dependability, structured output, and the ability to build workflows that run the same way every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  A few things to keep in mind
&lt;/h2&gt;

&lt;p&gt;This Actor is built for public Instagram content, and it's worth being clear about what that means in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Private content won't work.&lt;/strong&gt; If a post requires you to be logged in to view it, the Actor can't access it, and it will return an error item for that URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instagram can and does change things.&lt;/strong&gt; The page structure, video delivery logic, and anti-bot behavior can shift with Instagram updates. When that happens, the extraction may stop working for some URL types until the Actor is updated. The structured error output means you'll know which URLs failed rather than getting a silent empty result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're responsible for compliance.&lt;/strong&gt; Only download content you own, have explicit permission to use, or have a legitimate business reason to process. Respect copyright, platform terms, and local laws. The tool does what it's designed to do - how you use it is your responsibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Most Instagram video download tools are consumer utilities. They're built for "save this one Reel right now" - not for "process this batch of public videos, give me structured output, and let me automate it next week too."&lt;/p&gt;

&lt;p&gt;If your work touches Instagram video content at any real scale - campaign archives, content pipelines, creator workflows, or automated reporting - that distinction matters a lot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Instagram Video and Reel Downloader&lt;/a&gt; is built for the second use case. It gives you direct video URLs, structured metadata, optional file storage, a reliable fallback for difficult Reel pages, and a platform underneath it that supports APIs, scheduling, webhooks, and integrations.&lt;/p&gt;

&lt;p&gt;Try it from Apify Console with a few public URLs. The first run takes about few seconds to set up, and you'll see immediately whether it fits the workflow you're building.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/instagram-video-and-reel-downloader" rel="noopener noreferrer"&gt;Get started with Instagram Video and Reel Downloader on Apify →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>instagram</category>
      <category>api</category>
      <category>apify</category>
      <category>video</category>
    </item>
    <item>
      <title>How to scrape Nextdoor for hyper-local demographics and community sentiment</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Sat, 30 May 2026 07:05:14 +0000</pubDate>
      <link>https://dev.to/eunit/how-to-scrape-nextdoor-for-hyper-local-demographics-and-community-sentiment-2cbg</link>
      <guid>https://dev.to/eunit/how-to-scrape-nextdoor-for-hyper-local-demographics-and-community-sentiment-2cbg</guid>
      <description>&lt;p&gt;Neighborhood-level data is where many high-value business decisions start. Citywide averages are useful, but they often hide the details that matter when you are choosing where to advertise, where to invest, or how to understand what local communities care about. If you want to find areas with strong homeownership, identify places where residents discuss common service needs, or analyze how people describe a neighborhood in public community pages, you need hyper-local signals.&lt;/p&gt;

&lt;p&gt;That is exactly why so many teams look for a reliable way to scrape Nextdoor. The challenge is that &lt;a href="https://ctexaminer.com/2026/03/27/nextdoors-closed-door-policy/" rel="noopener noreferrer"&gt;Nextdoor is a relatively closed ecosystem&lt;/a&gt;. It does not offer an easy, public, developer-friendly path for extracting the kind of structured data marketers, analysts, founders, and researchers want. Manual collection is slow, inconsistent, and impossible to scale across hundreds of locations.&lt;/p&gt;

&lt;p&gt;This is where &lt;a href="https://apify.com/eunit/nextdoor-scraper?fpr=eunit" rel="noopener noreferrer"&gt;&lt;strong&gt;Nextdoor Scraper&lt;/strong&gt;&lt;/a&gt; helps. Built as one of the &lt;a href="https://apify.com/eunit/nextdoor-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Apify Actors&lt;/a&gt; for the Apify platform, it turns public Nextdoor city-page data into structured output you can use. Instead of copying information by hand, you can collect geographic coordinates, resident counts, income indicators, homeowner ratios, affordability signals, FAQ content, and local community references in a repeatable workflow. If you need a practical Nextdoor API alternative for public pages, this is the faster path.&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.amazonaws.com%2Fuploads%2Farticles%2Fk3jzl3ho7vzvn5pzdzxa.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.amazonaws.com%2Fuploads%2Farticles%2Fk3jzl3ho7vzvn5pzdzxa.png" alt="Nextdoor logo" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the Nextdoor Scraper?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/nextdoor-scraper?fpr=eunit" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; is a scraping and data extraction Actor for collecting structured data from public Nextdoor pages, especially city pages. It is designed for people who want usable output, not raw HTML. That means you get cleaned fields that can go straight into a spreadsheet, dashboard, CRM, enrichment pipeline, or reporting workflow.&lt;/p&gt;

&lt;p&gt;In simple terms, the &lt;a href="https://apify.com/eunit/nextdoor-scraper" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; visits public Nextdoor pages, extracts the visible and structured page data, and returns it in machine-readable formats such as JSON, XLSX, and CSV. That makes it easier to analyze neighborhood-level trends at scale without building your own browser automation stack from scratch.&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%2Fi.postimg.cc%2FdtTwsSDL%2Fscreenshot-2026-05-29-at-15-36-11.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%2Fi.postimg.cc%2FdtTwsSDL%2Fscreenshot-2026-05-29-at-15-36-11.png" alt="Aberdeen, Ohio - Nextdoor page" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The extracted fields are especially useful for local market analysis. Depending on the public page and the available structured data, the Actor can capture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Geographic coordinates, such as a centroid with latitude and longitude&lt;/li&gt;
&lt;li&gt;City and state details&lt;/li&gt;
&lt;li&gt;Resident counts&lt;/li&gt;
&lt;li&gt;Average age&lt;/li&gt;
&lt;li&gt;Average income&lt;/li&gt;
&lt;li&gt;Percentage of homeowners&lt;/li&gt;
&lt;li&gt;Affordability and friendliness scores&lt;/li&gt;
&lt;li&gt;FAQ entries&lt;/li&gt;
&lt;li&gt;Neighborhood references&lt;/li&gt;
&lt;li&gt;Business directory references&lt;/li&gt;
&lt;li&gt;Local groups and additional community resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a public city page can expose useful signals such as a centroid of &lt;code&gt;40.016, -80.747&lt;/code&gt;, a resident count of &lt;code&gt;3,878&lt;/code&gt;, an average age of &lt;code&gt;35&lt;/code&gt;, an average income of &lt;code&gt;$41k&lt;/code&gt;, and a homeowner share of &lt;code&gt;56%&lt;/code&gt;. It can also include livability indicators such as an affordability score of &lt;code&gt;99&lt;/code&gt; and a friendliness score of &lt;code&gt;86&lt;/code&gt;. Those values are much more actionable than a plain list of URLs because they help you compare locations quickly and prioritize where to spend time and budget.&lt;/p&gt;

&lt;p&gt;The result is a practical way to scrape Nextdoor data for research, local service lead generation, geographic analysis, and community sentiment discovery, while staying focused on public information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Top 4 profitable use cases for Nextdoor data
&lt;/h2&gt;

&lt;p&gt;The biggest reason to use a Nextdoor scraper is not the scraping itself. It is what the data helps you decide, launch, test, or sell afterward. Here are four high-value ways to use hyper-local demographics and community sentiment from public Nextdoor pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Building regional contractor and reputation platforms
&lt;/h3&gt;

&lt;p&gt;If you are building a &lt;a href="https://demandium.app/blog/best-home-service-marketplaces-platform/" rel="noopener noreferrer"&gt;regional directory, a home services marketplace, or a contractor reputation platform&lt;/a&gt;, you need more than a list of businesses. You need neighborhood context.&lt;/p&gt;

&lt;p&gt;Public Nextdoor data helps you map cities and surrounding local structures in a way that feels more grounded in how residents actually organize their communities. The FAQs, neighborhood references, and local groups can help you understand how people describe an area, what kinds of issues come up repeatedly, and which categories of services matter most.&lt;/p&gt;

&lt;p&gt;That is valuable when you are building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A local contractor comparison site&lt;/li&gt;
&lt;li&gt;A regional reputation database&lt;/li&gt;
&lt;li&gt;A neighborhood services recommendation engine&lt;/li&gt;
&lt;li&gt;A local SEO content network for service categories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of launching broad, generic pages, you can prioritize neighborhoods with meaningful public signals and create location-aware service experiences. That makes your product more relevant, and it can improve both engagement and conversion.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Lead generation for home services
&lt;/h3&gt;

&lt;p&gt;This is one of the clearest commercial use cases. Home service businesses such as roofers, landscapers, &lt;a href="https://blog.apify.com/plumbing-leads/" rel="noopener noreferrer"&gt;plumbers&lt;/a&gt;, painters, pest control companies, and HVAC providers need to know where homeowners live, where disposable income is high, and where local demand is likely to justify the cost of outreach. &lt;a href="https://apify.com/eunit/nextdoor-scraper/api/python" rel="noopener noreferrer"&gt;Nextdoor city-page data&lt;/a&gt; can help you build exactly that picture.&lt;/p&gt;

&lt;p&gt;For example, if a location shows &lt;code&gt;3,878&lt;/code&gt; residents, an average income of &lt;code&gt;$41k&lt;/code&gt;, and &lt;code&gt;56%&lt;/code&gt; homeowners, that is immediately more useful than running a generic campaign across an entire metro area. It gives you a better basis for deciding where to target direct mail, paid social, local landing pages, sales outreach, and neighborhood-specific offers.&lt;/p&gt;

&lt;p&gt;You can use the data to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Find areas with stronger homeowner density&lt;/li&gt;
&lt;li&gt;Compare income signals across nearby cities&lt;/li&gt;
&lt;li&gt;Prioritize communities for seasonal campaigns&lt;/li&gt;
&lt;li&gt;Match marketing messages to neighborhood concerns surfaced in FAQs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A landscaping company may care about homeowner-heavy areas. A roofing business may want to focus on suburban communities with stable ownership and strong local trust signals. A cleaning or handyman service may use local groups and neighborhood references to decide where to pilot a new offer. That is the real advantage of scraping Nextdoor for lead generation. You are getting closer to the local context behind the customer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Real estate investment and market research
&lt;/h3&gt;

&lt;p&gt;Real estate teams already use public data from many sources, but the hyper-local context is often fragmented. That is where public Nextdoor data becomes interesting.&lt;/p&gt;

&lt;p&gt;When a city page includes livability indicators such as an affordability score of &lt;code&gt;99&lt;/code&gt; and a friendliness score of &lt;code&gt;86&lt;/code&gt;, you gain another layer of context for evaluating a market. These values do not replace formal due diligence, but they can help analysts screen locations faster before moving into deeper research.&lt;/p&gt;

&lt;p&gt;A real estate investor, brokerage, or proptech company can use the data to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compare neighborhoods and cities at a glance&lt;/li&gt;
&lt;li&gt;Identify areas that look affordable relative to nearby markets&lt;/li&gt;
&lt;li&gt;Spot locations with strong community-oriented signals&lt;/li&gt;
&lt;li&gt;Enrich internal market reports with public local context&lt;/li&gt;
&lt;li&gt;Create lead magnets and market snapshots for clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Geographic coordinates also matter. A centroid such as &lt;code&gt;40.016, -80.747&lt;/code&gt; makes it easier to map results, visualize city clusters, and combine the output with other location-based datasets. That is useful for market scoring, territory planning, and neighborhood-level dashboards.&lt;/p&gt;

&lt;p&gt;If you are producing local real estate data products, public Nextdoor scraping can work as a lightweight enrichment layer that adds community context to your pipeline.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Community sentiment and trend analysis
&lt;/h3&gt;

&lt;p&gt;Community sentiment is often hard to quantify because it spans many channels. Nextdoor is valuable because it is built around neighborhood-level discussion, recommendations, local concerns, and community interaction.&lt;/p&gt;

&lt;p&gt;Even when you focus only on public pages, FAQ sections, local groups, business directory references, and related resources can still reveal what a place emphasizes. That helps you understand whether a community leans toward family life, local commerce, safety, neighborhood organization, pet activity, home improvement, or other recurring themes.&lt;/p&gt;

&lt;p&gt;This is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local market researchers&lt;/li&gt;
&lt;li&gt;Urban policy teams&lt;/li&gt;
&lt;li&gt;Media analysts&lt;/li&gt;
&lt;li&gt;Economic development groups&lt;/li&gt;
&lt;li&gt;Brands planning geo-targeted campaigns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, if you see repeated references to neighborhood groups, practical local services, and community FAQs, that can point to a highly engaged local audience. If business directories are prominent, it may suggest strong local commercial activity. If certain topic clusters appear across multiple nearby pages, you may have found a regional trend worth tracking over time. In other words, the value is not just in scraping Nextdoor data once. It is in building a repeatable view of how communities present themselves publicly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key features of the Nextdoor Scraper
&lt;/h2&gt;

&lt;p&gt;The appeal of &lt;a href="https://apify.com/eunit/nextdoor-scraper" rel="noopener noreferrer"&gt;Nextdoor scraper&lt;/a&gt; is that it is not just a one-off script. It is packaged as one of the &lt;a href="https://apify.com/store?fpr=eunit" rel="noopener noreferrer"&gt;Apify Actors&lt;/a&gt;, so it fits into a repeatable workflow on the Apify platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed and scale
&lt;/h3&gt;

&lt;p&gt;Instead of manually opening pages and copying details into a spreadsheet, you can run automated extraction across many public URLs in one job. That saves hours of repetitive work and makes side-by-side comparison possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured output for analysis
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://apify.com/eunit/nextdoor-scraper" rel="noopener noreferrer"&gt;Nextdoor scraper&lt;/a&gt; returns normalized data fields, not just raw page content. That makes it easier to filter, group, sort, enrich, and export your results for business use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser-based extraction on the Apify platform
&lt;/h3&gt;

&lt;p&gt;Nextdoor pages are modern web pages, so browser automation matters. The &lt;a href="https://apify.com/eunit/nextdoor-scraper" rel="noopener noreferrer"&gt;Nextdoor scraper&lt;/a&gt; handles the browser-driven extraction flow for you on the &lt;a href="https://console.apify.com/sign-up?fpr=eunit" rel="noopener noreferrer"&gt;Apify platform&lt;/a&gt;, which is much easier than maintaining your own extraction stack and local tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost-effective automation
&lt;/h3&gt;

&lt;p&gt;Building and maintaining your own pipeline for public-page collection can get expensive fast. Using &lt;a href="https://apify.com/store?fpr=eunit" rel="noopener noreferrer"&gt;Apify Actors&lt;/a&gt; reduces engineering overhead, shortens time to value, and gives you a practical way to pay for execution instead of building everything yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Export formats that fit real workflows
&lt;/h3&gt;

&lt;p&gt;Once the data is collected, you can work with it in formats your team already uses. JSON is ideal for developers and downstream automation. CSV is ideal for analysts, operations teams, and quick spreadsheet reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-step tutorial: how to scrape Nextdoor data
&lt;/h2&gt;

&lt;p&gt;Using Nextdoor Scraper on the Apify platform is straightforward.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Access the Actor
&lt;/h3&gt;

&lt;p&gt;Open &lt;a href="https://apify.com/eunit/nextdoor-scraper" rel="noopener noreferrer"&gt;&lt;strong&gt;Nextdoor Scraper&lt;/strong&gt;&lt;/a&gt; in Apify Store. On the Actor page, click &lt;strong&gt;Try for free&lt;/strong&gt; or &lt;strong&gt;Start&lt;/strong&gt; to open the input form in Apify Console.&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%2Fi.postimg.cc%2FT3X14Ycs%2FNextdoor-Apify.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%2Fi.postimg.cc%2FT3X14Ycs%2FNextdoor-Apify.png" alt="Nextdoor Scraper" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configure the target URLs
&lt;/h3&gt;

&lt;p&gt;In the &lt;strong&gt;Input&lt;/strong&gt; tab, add one or more public Nextdoor URLs. A common starting point is a city page, such as:&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;"startUrls"&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://nextdoor.com/city/north-miami--fl/"&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;"maxRequestsPerCrawl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxConcurrency"&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;"maxItemsPerSection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fi.postimg.cc%2F9FNHnWjh%2Fscreenshot-2026-05-30-at-07-34-21.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%2Fi.postimg.cc%2F9FNHnWjh%2Fscreenshot-2026-05-30-at-07-34-21.png" alt="Nextdoor Scraper - Input configuration" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Run the Actor
&lt;/h3&gt;

&lt;p&gt;Click the &lt;strong&gt;Start&lt;/strong&gt; button at the bottom of the page. The Actor initializes, processes the target pages, extracts structured data fields, and stores the results in a dataset.&lt;/p&gt;

&lt;p&gt;Depending on the page, the output can include fields such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cityName&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;centroid&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;residentsCount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;averageAge&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;averageIncome&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;percentageHomeowners&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;affordabilityScore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;friendlinessScore&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;faqs&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;localGroups&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Export the dataset
&lt;/h3&gt;

&lt;p&gt;Once the run finishes, open the &lt;strong&gt;Storage&lt;/strong&gt; tab to inspect the extracted records. You can then export the dataset in the format that best fits your workflow.&lt;/p&gt;

&lt;p&gt;Here is a simplified example of the kind of output you can expect:&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="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://nextdoor.com/city/example-city/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cityName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example City"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"centroid"&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;"latitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;40.016&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"longitude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-80.747&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;"residentsCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3878&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"averageAge"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"averageIncome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$41k"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"percentageHomeowners"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"56%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"affordabilityScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"friendlinessScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;86&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"faqCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accessStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, you can download the results as JSON or CSV and move directly into analysis, enrichment, reporting, or outreach planning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrating Nextdoor data into your workflows
&lt;/h2&gt;

&lt;p&gt;The value of scraped Nextdoor data increases when it flows into the tools your team already uses.&lt;/p&gt;

&lt;p&gt;On the Apify platform, you can connect dataset outputs to automations and downstream systems. That includes sending results to Google Sheets for review, using Make for no-code workflows, or routing data into Zapier for notifications and follow-up actions. You can also plug the JSON output into your own analytics stack, internal dashboards, CRMs, or lead scoring systems.&lt;/p&gt;

&lt;p&gt;This is especially helpful when you are monitoring multiple cities over time. Instead of exporting data manually after each run, you can make Nextdoor scraping into a repeatable process that supports lead generation, market tracking, and local intelligence workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is scraping Nextdoor legal?
&lt;/h2&gt;

&lt;p&gt;Scraping should always be done responsibly. The safest approach is to focus on publicly available data, respect applicable rules and terms, and avoid collecting private or personal information behind login walls.&lt;/p&gt;

&lt;p&gt;Nextdoor Scraper is designed around public page extraction. That means the workflow is intended for public city-page data and other publicly accessible content, not private profiles, direct messages, or restricted user data. You should also review your own legal obligations, compliance needs, and usage policies before running large-scale data collection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If you want to scrape Nextdoor for hyper-local demographics, local service lead generation, neighborhood research, or community sentiment analysis, structured public-page extraction gives you a strong starting point. Instead of relying on manual research, you can collect repeatable location data such as resident counts, income signals, homeowner ratios, affordability indicators, friendliness scores, FAQs, and local group references in a format your team can actually use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apify.com/eunit/nextdoor-scraper" rel="noopener noreferrer"&gt;Nextdoor Scraper&lt;/a&gt; turns that process into a scalable workflow on the Apify platform. Open the Actor in Apify Store, run your first extraction, and start building better local market insights today.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>leadgen</category>
      <category>realestate</category>
    </item>
    <item>
      <title>How to get YouTube data for a growth strategy: video and channel analysis</title>
      <dc:creator>Emmanuel Uchenna</dc:creator>
      <pubDate>Thu, 09 Apr 2026 17:02:04 +0000</pubDate>
      <link>https://dev.to/eunit/how-to-get-youtube-data-for-a-growth-strategy-video-and-channel-analysis-7a1</link>
      <guid>https://dev.to/eunit/how-to-get-youtube-data-for-a-growth-strategy-video-and-channel-analysis-7a1</guid>
      <description>&lt;p&gt;YouTube has more than 2 billion logged-in users every month and &lt;a href="https://www.statista.com/statistics/259477/hours-of-video-uploaded-to-youtube-every-minute/?srsltid=AfmBOopPcf0RFGQdL-oJ74szr6mWMInPT0Y3cQK4pdSRPbcfAxq1B_Wj" rel="noopener noreferrer"&gt;processes over 500 hours of uploaded video every minute&lt;/a&gt;. It is the second-largest search engine in the world. Yet the majority of creators and brands still build their content strategy the old-fashioned way: watching what competitors post, guessing which topics will resonate, and hoping the algorithm cooperates.&lt;/p&gt;

&lt;p&gt;That approach works early on. But as your niche becomes more competitive and your publishing schedule more demanding, manual research stops scaling. You can't audit 20 competitor channels by hand every week, track which video formats are trending across five countries simultaneously, and still have time to actually make content.&lt;/p&gt;

&lt;p&gt;What changes everything is structured data. When you can collect trending video metrics, popular channel signals, and keyword data programmatically - across countries, categories, and time - YouTube growth becomes a repeatable system rather than a creative gamble.&lt;/p&gt;

&lt;p&gt;This article shows you how to obtain that data using two purpose-built &lt;a href="https://apify.com/store?fpr=eunit" rel="noopener noreferrer"&gt;Apify Actors&lt;/a&gt;, and how to turn it into a growth strategy grounded in what the platform is actually rewarding right now.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why YouTube data matters for growth strategy
&lt;/h2&gt;

&lt;p&gt;YouTube operates on two distinct engines: search and recommendations. Search surfaces content in response to specific queries. Recommendations push content to viewers who weren't actively searching - but whose watch history signals a relevant interest. A strong growth strategy needs both, and both require different types of data.&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.amazonaws.com%2Fuploads%2Farticles%2Focnns11gey0acp3fqy03.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.amazonaws.com%2Fuploads%2Farticles%2Focnns11gey0acp3fqy03.png" alt="YouTube data for a growth strategy: video and channel analysis" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The data that matters most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trending videos by category and country&lt;/strong&gt; - what the recommendation algorithm is actively amplifying, which formats and topics are getting pushed to new audiences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Popular channels and their recent content&lt;/strong&gt; - which creators the platform is currently favoring, and what they're publishing right now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Popular keywords&lt;/strong&gt; - what viewers are actively searching for at this moment, not six months ago&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement metrics (views, likes, comments)&lt;/strong&gt; - signals of content-audience fit that go deeper than raw view counts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The official YouTube Data API was not designed for this kind of research. Its quota limits cap how much data you can pull per day, it has no trending channels endpoint, and accessing granular trend data by country and category requires complex workarounds that hit limits quickly. For recurring competitive research, the API becomes an obstacle rather than a tool.&lt;/p&gt;

&lt;p&gt;Purpose-built scraping Actors solve this. The &lt;a href="https://apify.com/eunit/youtube-trending-videos-by-categories" rel="noopener noreferrer"&gt;YouTube Trending Videos by Categories Scraper&lt;/a&gt; and &lt;a href="https://apify.com/eunit/youtube-popular-channels-scraper" rel="noopener noreferrer"&gt;YouTube Popular Channels Scraper&lt;/a&gt; extract exactly what you need - structured, export-ready data - without API keys, quota management, or engineering overhead.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want a step-by-step scraping walkthrough before diving into strategy, read &lt;a href="https://www.eunit.me/blog/how-to-scrape-youtube-trends-and-popular-channels" rel="noopener noreferrer"&gt;How to scrape YouTube trends and popular channels&lt;/a&gt; first.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The growth data you can collect and why it matters
&lt;/h2&gt;

&lt;p&gt;Before looking at how to use the data, it helps to understand exactly what each Actor returns and which growth question each data point answers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data point&lt;/th&gt;
&lt;th&gt;Source Actor&lt;/th&gt;
&lt;th&gt;Growth use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trending video rank, title, views, likes&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/eunit/youtube-trending-videos-by-categories" rel="noopener noreferrer"&gt;YouTube Trending Videos by Categories Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Spot viral formats and topics before your competitors do&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;published_time_text&lt;/code&gt; (e.g., "Yesterday", "3 hours ago")&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/eunit/youtube-trending-videos-by-categories" rel="noopener noreferrer"&gt;YouTube Trending Videos by Categories Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Identify how quickly content trends and optimal publishing windows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video thumbnail URL&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/eunit/youtube-trending-videos-by-categories" rel="noopener noreferrer"&gt;YouTube Trending Videos by Categories Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Analyze thumbnail patterns that correlate with high click-through rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Channel name, recent videos, video URLs&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/eunit/youtube-popular-channels-scraper" rel="noopener noreferrer"&gt;YouTube Popular Channels Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Identify which channels are dominating your niche right now&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Popular keywords&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/eunit/youtube-popular-channels-scraper" rel="noopener noreferrer"&gt;YouTube Popular Channels Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Discover high-intent search terms for SEO and content ideation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category breakdown per country&lt;/td&gt;
&lt;td&gt;Both Actors&lt;/td&gt;
&lt;td&gt;Understand regional audience preferences for localized content strategies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engagement ratio (likes / views)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://apify.com/eunit/youtube-trending-videos-by-categories" rel="noopener noreferrer"&gt;YouTube Trending Videos by Categories Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Filter for content that converts viewers, not just attracts them&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Together, these two Actors give you a full picture view of what YouTube is amplifying in your space - the videos rising to the top and the channels being pushed to new audiences.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using trending video data for content strategy
&lt;/h2&gt;

&lt;p&gt;Trending videos are the algorithm's endorsement. They represent the exact combination of topic, format, title structure, and production style that YouTube is choosing to amplify in a given country and category at a specific moment. Analyzing these patterns systematically - rather than browsing trending pages manually - reveals what's working before it becomes obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to collect trending video data
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://apify.com/eunit/youtube-trending-videos-by-categories" rel="noopener noreferrer"&gt;YouTube Trending Videos by Categories Scraper&lt;/a&gt; on Apify. Create a free account if you don't have one.&lt;/li&gt;
&lt;li&gt;Select your target &lt;strong&gt;Country&lt;/strong&gt; (e.g., &lt;code&gt;united-states&lt;/code&gt;, &lt;code&gt;united-kingdom&lt;/code&gt;, &lt;code&gt;india&lt;/code&gt;, &lt;code&gt;brazil&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Select your &lt;strong&gt;Category&lt;/strong&gt; (e.g., &lt;code&gt;gaming&lt;/code&gt;, &lt;code&gt;music&lt;/code&gt;, &lt;code&gt;howto-and-style&lt;/code&gt;, &lt;code&gt;science-and-technology&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save &amp;amp; Start&lt;/strong&gt;. The run completes in seconds.&lt;/li&gt;
&lt;li&gt;Preview results in the table view or export as JSON, CSV, or Excel.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each result includes: rank, video title, video URL, thumbnail URL, author (channel name), &lt;code&gt;published_time_text&lt;/code&gt;, views, likes, and comments.&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"video_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;"Marvel Television's Wonder Man | Official Trailer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"video_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://youtube.com/watch?v=wHuWmjXsReU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Marvel Entertainment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"published_time_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;"Yesterday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"views"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5.2M"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"likes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"113K"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"comments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4.9K"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"united-kingdom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"All"&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;Run this Actor across multiple countries and categories, and you have a competitive intelligence layer that updates every time you trigger it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Competitor channel analysis: who's winning and why
&lt;/h2&gt;

&lt;p&gt;Channel analysis is where most creators focus - but most do it wrong. They look at subscriber counts. A channel with 800k subscribers might not have posted in two months and could be algorithmically invisible. The more useful signal is which channels are &lt;em&gt;currently appearing as popular&lt;/em&gt; in trending data - because that reflects what YouTube is actively recommending to new viewers right now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why popular channel data beats subscriber counts
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://apify.com/eunit/youtube-popular-channels-scraper" rel="noopener noreferrer"&gt;YouTube Popular Channels Scraper&lt;/a&gt; returns the channels YouTube is currently surfacing as "buzzing" in a given country and category, along with their most recent videos. This is fundamentally different from a ranked list of the biggest channels. A channel with 80k subscribers appearing here is actively growing. A channel with 2 million subscribers that doesn't appear has lost algorithmic favor.&lt;/p&gt;

&lt;p&gt;This distinction matters enormously for competitor research. Your real YouTube competitors are not necessarily your biggest industry rivals - they're the channels competing for the same viewer attention and winning right now.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to analyze in popular channel data
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Publishing frequency and recency.&lt;/strong&gt; How many videos has each trending channel posted in the last 30 days? Channels maintaining a consistent 2-3 videos per week in trending data signal that the algorithm rewards their cadence. That's a publishing frequency benchmark, not just an aspiration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topic and title patterns.&lt;/strong&gt; Across a channel's recent trending videos, which topics repeat? If a competitor's recent 6 trending videos are all about personal finance for under-30s, that sub-niche is clearly working for their audience. Look for the intersection between their most successful topics and gaps in your own content calendar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-country channel overlap.&lt;/strong&gt; Run the Actor for the same category across the US, UK, and Australia. Channels appearing in all three are building genuine algorithmic momentum across English-speaking markets. These are the channels worth monitoring most closely.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to collect popular channel data
&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.amazonaws.com%2Fuploads%2Farticles%2Fp4i5zqnj7knkrr0jux4m.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.amazonaws.com%2Fuploads%2Farticles%2Fp4i5zqnj7knkrr0jux4m.png" alt="YouTube Popular Channels Scraper" width="800" height="116"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://apify.com/eunit/youtube-popular-channels-scraper" rel="noopener noreferrer"&gt;YouTube Popular Channels Scraper&lt;/a&gt; on Apify.&lt;/li&gt;
&lt;li&gt;Select your &lt;strong&gt;Country&lt;/strong&gt; and &lt;strong&gt;Category&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save &amp;amp; Start&lt;/strong&gt;. Results include channel names, recent video URLs, and popular keywords for the selected market.&lt;/li&gt;
&lt;li&gt;Export and repeat for 3-5 target countries.&lt;/li&gt;
&lt;li&gt;Build a simple tracking sheet: channel name, countries trending in, recent topic themes, and estimated posting frequency. Update it weekly.
&lt;/li&gt;
&lt;/ol&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="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"united-kingdom"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"all"&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;h2&gt;
  
  
  Building a multi-country growth intelligence system
&lt;/h2&gt;

&lt;p&gt;A one-off data pull gives you a snapshot. A repeatable system gives you a competitive advantage that compounds over time. Here's how to build one without a development team.&lt;/p&gt;

&lt;h3&gt;
  
  
  The architecture
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Collect&lt;/strong&gt; - run both Actors weekly across your target countries (3-5) and category&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store&lt;/strong&gt; - export results to Google Sheets or Airtable with a date column so you can track changes over time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analyze&lt;/strong&gt; - compare week-over-week: are the same channels trending, or new ones emerging? Are keywords shifting? Are engagement ratios moving up or down?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act&lt;/strong&gt; - translate the top 3 insights into your next 2 weeks of content: one trending topic, one underserved format, one keyword to target&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Scheduling automatic runs
&lt;/h3&gt;

&lt;p&gt;From Apify Console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure your Actor run (country + category)&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save as a new task&lt;/strong&gt; and give it a name (e.g., "US Gaming Trends - Weekly")&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Schedules&lt;/strong&gt; in the left navigation and click &lt;strong&gt;Create new schedule&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add your task and set the frequency - weekly is a good starting cadence&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No code required. The Actor runs automatically, and new results are appended to the dataset.&lt;/p&gt;

&lt;h3&gt;
  
  
  For developers: automate with Python
&lt;/h3&gt;

&lt;p&gt;If you're building a dashboard, feeding a machine learning model, or integrating this data into a larger pipeline, the &lt;code&gt;apify-client&lt;/code&gt; makes automation straightforward.&lt;/p&gt;

&lt;p&gt;Install the 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;apify-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the YouTube Popular Channels Scraper programmatically:&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;apify_client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ApifyClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApifyClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;run_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&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;united-states&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&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;gaming&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;run&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;actor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eunit/youtube-popular-channels-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_input&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;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defaultDatasetId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;iterate_items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="err"&gt;&amp;nbsp;&lt;/span&gt; &lt;span class="err"&gt;&amp;nbsp;&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;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;eunit/youtube-popular-channels-scraper&lt;/code&gt; for &lt;code&gt;eunit/youtube-trending-videos-by-categories&lt;/code&gt; to pull trending video data instead. The input schema is identical. You can run both Actors in sequence and merge the outputs into a single structured dataset for a complete weekly intelligence snapshot.&lt;/p&gt;

&lt;p&gt;Your API token is in &lt;strong&gt;Settings &amp;gt; API &amp;amp; Integrations&lt;/strong&gt; in Apify Console.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pricing: what this costs in practice
&lt;/h2&gt;

&lt;p&gt;Both Actors use a Pay-Per-Event pricing model. You are charged a fixed amount per result successfully scraped - not for platform compute time or server usage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Actor&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://apify.com/eunit/youtube-trending-videos-by-categories" rel="noopener noreferrer"&gt;YouTube Trending Videos by Categories Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;from $1.00 / 1,000 results&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://apify.com/eunit/youtube-popular-channels-scraper" rel="noopener noreferrer"&gt;YouTube Popular Channels Scraper&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;from $1.90 / 1,000 results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In practice, running a weekly trend scan across 5 countries with 1 category each returns approximately 200-300 data points - costing under $0.30 per run, or roughly $1.20/month.&lt;/p&gt;

&lt;p&gt;Traditional SaaS YouTube analytics platforms typically charge between $49 and $299/month for dashboards with less granular, less current data than you get from a direct scrape. The Pay-Per-Event model means you pay exactly in proportion to how much data you need. Scale up for a product launch. Scale down during quiet weeks. No subscriptions, no idle spend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Legal and ethical considerations
&lt;/h2&gt;

&lt;p&gt;Both Actors collect publicly available data - information that any user can see by visiting YouTube without logging in. They do not access private videos, user passwords, private messages, or any content that requires authentication.&lt;/p&gt;

&lt;p&gt;Scraping public data for research, competitive intelligence, and informational purposes is generally considered acceptable in most jurisdictions. Operate within YouTube's Terms of Service, use the data for your own analysis, and avoid any use cases that involve republishing scraped content without appropriate attribution.&lt;/p&gt;

&lt;p&gt;For Apify's broader guidance on ethical web scraping, see the &lt;a href="https://docs.apify.com" rel="noopener noreferrer"&gt;Apify documentation&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;YouTube growth stops being a guessing game when you treat it like a data problem. Trending videos tell you what the algorithm is amplifying right now. Popular channel data shows you who it's amplifying and why. Engagement ratios reveal which content is building real audience relationships versus passive viewership. And popular keywords hand you the exact search terms your potential viewers are using today.&lt;/p&gt;

&lt;p&gt;The growth loop becomes repeatable: collect data weekly, identify the 2-3 highest-signal patterns, build your next content batch around them, and measure whether your own engagement metrics are converging with trending benchmarks.&lt;/p&gt;

&lt;p&gt;The best channels in every niche are already doing this. The ones that aren't are still guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start collecting:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://apify.com/eunit/youtube-trending-videos-by-categories" rel="noopener noreferrer"&gt;YouTube Trending Videos by Categories Scraper&lt;/a&gt; - from $1.00 / 1,000 results&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apify.com/eunit/youtube-popular-channels-scraper" rel="noopener noreferrer"&gt;YouTube Popular Channels Scraper&lt;/a&gt; - from $1.90 / 1,000 results&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;Can I get YouTube trending data without the official API?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. The &lt;a href="https://apify.com/eunit/youtube-trending-videos-by-categories" rel="noopener noreferrer"&gt;YouTube Trending Videos by Categories Scraper&lt;/a&gt; extracts real-time trending data across 25+ countries and 13 categories with no API key required and no quota limits to manage. You get the same data you'd see by browsing YouTube's trending pages, in a structured, export-ready format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How is "popular channel" data different from subscriber count?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Popular channel data reflects which channels YouTube is actively recommending to new viewers right now. A channel with 80k subscribers can appear here because it's publishing consistently and gaining algorithmic momentum. A channel with 2 million subscribers that hasn't posted recently won't. For competitive intelligence, current algorithmic favor matters far more than historical growth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What categories can I track?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both Actors support: All, Music, Gaming, Film &amp;amp; Animation, Autos &amp;amp; Vehicles, Sports, Pets &amp;amp; Animals, Comedy, Entertainment, News &amp;amp; Politics, How-to &amp;amp; Style, and Science &amp;amp; Technology - across 25+ countries including the US, UK, India, Brazil, Japan, Germany, Australia, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often should I run the scrapers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Weekly is sufficient for most content strategies. Daily runs are useful during product launches, campaigns, or for fast-moving niches like Gaming or News &amp;amp; Politics where trends shift within hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use this data for sentiment analysis?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Engagement ratios (likes-to-views, comment volume relative to views) are a reliable sentiment proxy at scale and require no comment scraping. For comment-level sentiment - what viewers are actually saying - pair these Actors with a YouTube Comments Scraper and feed the output to an LLM for summarization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is YouTube scraping legal?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scraping publicly available YouTube data for research and informational purposes is generally legal in most jurisdictions. These Actors only collect data visible to any visitor without logging in. Always use scraped data in accordance with YouTube's Terms of Service and applicable privacy laws.&lt;/p&gt;

</description>
      <category>youtube</category>
      <category>seo</category>
      <category>contentstrategy</category>
      <category>datascraping</category>
    </item>
  </channel>
</rss>
