β¨ Group a big list of keywords into topic clusters based on how much their Google results overlap.
If two keywords are served by the same pages in Google, they usually share the same search intent β so you should target them with one page, not two. This API finds those groups automatically, so you know exactly how many pages to create and which keywords belong on each.
Below are 5 keywords with the URLs that rank for each in Google. Hit Run Clustering and watch them collapse into topic groups. Edit the JSON to try your own data.
Run the demo to see the response.
The 5 keywords above collapse into 2 clusters β because Google serves them with overlapping pages:
| Cluster | Keywords | Total Volume | Intent |
|---|---|---|---|
| best running shoes | best running shoes, top running shoes, running shoes reviews | 70,700 | π Commercial / reviews |
| how to clean running shoes | how to clean running shoes, running shoe cleaning tips | 7,300 | π§½ How-to / informational |
The payoff: instead of writing 5 separate pages, you write 2 β one buyer's guide targeting the top cluster (70,700 searches/mo) and one how-to article for the second. The keywords in each cluster become your H2s and supporting terms.
Send a POST request to /sep-based-clustering/ with a JSON body:
serp_df β your SERP data as index-keyed columns (the format pandas produces with df.to_dict()). It has three keys:
Keyword β {"0": "keyword a", "1": "keyword b", ...}Volume β {"0": 1000, "1": 2000, ...} monthly search volume per keywordURLs β {"0": ["url1", "url2", ...], ...} the URLs that rank for each keywordβ οΈ The keys of Keyword, Volume, and URLs must line up (row "0" is the same keyword across all three). Use plain lists and the request will fail β it expects the index-keyed object format shown above.
common_num β minimum number of shared URLs required for two keywords to be considered the same topic (default: 4). Lower = looser/bigger clusters, higher = stricter/smaller clusters.hard_clustering β (default: true).
The response mirrors the input format β index-keyed columns, where each key is a row shared across every field:
{
"Keyword": {
"0": "best running shoes", "1": "top running shoes", "2": "running shoes reviews",
"3": "how to clean running shoes", "4": "running shoe cleaning tips"
},
"Volume": { "0": 40500, "1": 18100, "2": 12100, "3": 5400, "4": 1900 },
"Cluster Name": {
"0": "best running shoes", "1": "best running shoes", "2": "best running shoes",
"3": "how to clean running shoes", "4": "how to clean running shoes"
},
"Number of Keywords in Cluster": { "0": 3, "1": 3, "2": 3, "3": 2, "4": 2 },
"URLs": {
"0": "runnersworld.com,nike.com,adidas.com,amazon.com,rei.com",
"1": "runnersworld.com,nike.com,adidas.com,amazon.com,footlocker.com",
"2": "runnersworld.com,nike.com,adidas.com,wirecutter.com,rei.com",
"3": "wikihow.com,cleanipedia.com,nikecleaning.com,youtube.com,reddit.com",
"4": "wikihow.com,cleanipedia.com,youtube.com,goodhousekeeping.com,reddit.com"
}
}
Errors are returned as a JSON object with an error field, e.g.:
{ "error": "'list' object has no attribute 'values'" }
This particular message means serp_df was sent as plain lists instead of the index-keyed object format described above.