<?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: CountOnce</title>
    <description>The latest articles on DEV Community by CountOnce (@countonce).</description>
    <link>https://dev.to/countonce</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%2F4245270%2F2013e57e-3254-47e6-bfa4-ff37897da161.jpeg</url>
      <title>DEV Community: CountOnce</title>
      <link>https://dev.to/countonce</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/countonce"/>
    <language>en</language>
    <item>
      <title>Your keyword report is probably double-counting. Here is how to fix it</title>
      <dc:creator>CountOnce</dc:creator>
      <pubDate>Sun, 15 Nov 2026 10:35:41 +0000</pubDate>
      <link>https://dev.to/countonce/your-keyword-report-is-probably-double-counting-here-is-how-to-fix-it-10db</link>
      <guid>https://dev.to/countonce/your-keyword-report-is-probably-double-counting-here-is-how-to-fix-it-10db</guid>
      <description>&lt;p&gt;If you have ever pasted a keyword list into a volume tool and summed the column, there is a good chance your total was wrong. It was not a little wrong, either.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: Google counts close variants once
&lt;/h2&gt;

&lt;p&gt;Google Ads Keyword Planner does not report a separate figure for every way of writing a phrase. Plurals, word order and small misspellings are grouped together, and the group gets &lt;strong&gt;one&lt;/strong&gt; number.&lt;/p&gt;

&lt;p&gt;Here is a real lookup we ran this week (United States, Google Search):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Keyword&lt;/th&gt;
&lt;th&gt;Searches / month&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;standing desk&lt;/td&gt;
&lt;td&gt;135,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;standing desks&lt;/td&gt;
&lt;td&gt;135,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;standing desk converter&lt;/td&gt;
&lt;td&gt;18,100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sum the column and you get &lt;strong&gt;288,100&lt;/strong&gt;. But "standing desk" and "standing desks" are the &lt;em&gt;same&lt;/em&gt; 135,000 searches reported twice. The real total is &lt;strong&gt;153,100&lt;/strong&gt;. The naive sum overstates demand by 88%.&lt;/p&gt;

&lt;p&gt;It gets worse with longer lists. Paste four variants of one phrase ("running shoes", "running shoe", "shoes running", "runing shoes") into a typical tool and one of two things happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You get four rows of 246,000 that add up to &lt;strong&gt;984,000 searches that do not exist&lt;/strong&gt;, or&lt;/li&gt;
&lt;li&gt;Three of your keywords silently disappear from the output, and you never find out which ones.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither is acceptable if the number ends up in a client report, a market-size slide or a content plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why tools do this
&lt;/h2&gt;

&lt;p&gt;Keyword Planner returns one row per &lt;em&gt;group&lt;/em&gt;, not per keyword you sent. A tool that maps the response back to your input has to choose: repeat the group figure on every variant (inflated totals) or drop the keywords that did not come back (missing rows). Most tools quietly pick one of the two and never tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;We built a small API on Apify that does the boring part properly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One row for every keyword you sent.&lt;/strong&gt; Nothing disappears.&lt;/li&gt;
&lt;li&gt;Variants Google counts together share a &lt;code&gt;variantGroup&lt;/code&gt; id.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;countsTowardTotal&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; for exactly one keyword per group. Sum only those rows and your total is right.&lt;/li&gt;
&lt;li&gt;Every run stores a summary with both &lt;code&gt;totalVolumeNaive&lt;/code&gt; and &lt;code&gt;totalVolumeDeduplicated&lt;/code&gt;, so you can show the difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A variant group is only formed when the volume &lt;strong&gt;and all twelve monthly values&lt;/strong&gt; are identical &lt;em&gt;and&lt;/em&gt; the wording is close (plural, word order, a one- or two-letter difference). Unrelated keywords that happen to share a number are left apart.&lt;/p&gt;

&lt;p&gt;Each row also has CPC, the top-of-page bid range, competition, the 12-month series, peak month and a seasonality index. You do not need a Google Ads account or an API key from Google.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in one request
&lt;/h2&gt;

&lt;p&gt;The Actor has an instant HTTP endpoint. Send keywords, get JSON back in the same response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://countonce--keyword-search-volume-api.apify.actor/?keywords=standing%20desk,standing%20desks,standing%20desk%20converter&amp;amp;location=United%20States"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;APIFY_TOKEN&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response (shortened):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"requested"&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="nl"&gt;"variantGroups"&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="nl"&gt;"totalVolumeNaive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;288100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"totalVolumeDeduplicated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;153100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"rows"&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="nl"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"standing desk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nl"&gt;"searchVolume"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;135000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"cpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;5.18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"variantGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"g1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"countsTowardTotal"&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="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"standing desks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"searchVolume"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;135000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"cpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;5.18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"variantGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"g1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"countsTowardTotal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="nl"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"standing desk converter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"searchVolume"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"cpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"variantGroup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"countsTowardTotal"&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="nl"&gt;"peakMonth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"December"&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;For longer lists (up to 1,000 keywords), POST the same JSON you would use as the Actor input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -X POST &lt;span class="s2"&gt;"https://countonce--keyword-search-volume-api.apify.actor/"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;APIFY_TOKEN&amp;gt;"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d &lt;span class="s1"&gt;'{"keywords": ["running shoes", "trail running shoes"], "location": "Germany", "language": "de"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also works as a normal Apify run (paste keywords in the form, press Start), from n8n or Make with an HTTP Request node, and from AI agents through the Apify MCP server. If you hand the data to an LLM, tell it to sum &lt;code&gt;searchVolume&lt;/code&gt; only where &lt;code&gt;countsTowardTotal&lt;/code&gt; is true. Otherwise it will double-count, just like a spreadsheet would.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing, plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;$0.25 per live request (up to 1,000 keywords) + $0.0035 per keyword that has data.&lt;/li&gt;
&lt;li&gt;20 keywords cost $0.32, 200 cost $0.95, 1,000 cost $3.75.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free:&lt;/strong&gt; keywords with no data, invalid keywords, merged variants, and anything your account already fetched for the same market in the last 30 days.&lt;/li&gt;
&lt;li&gt;You can set a maximum cost per run, and the Actor will not go over it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data is Google Ads Keyword Planner data, supplied by DataForSEO.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Even if you never use our tool, check your own keyword sheets for duplicated numbers. If two keywords show the same volume and the same 12-month curve, Google has counted them as one. Keep one row in the total, not both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actor:&lt;/strong&gt; &lt;a href="https://apify.com/countonce/keyword-search-volume-api" rel="noopener noreferrer"&gt;Google Ads Keyword Search Volume API on Apify&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: we built and maintain this Actor.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
