πŸ”¬ SERP-Based Keyword Clustering API

🏠 Back to Home

✨ 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.

πŸš€ Why use it?

  • πŸ—ΊοΈ Build a content plan in minutes β€” turn 1,000 raw keywords into a short list of pages to write.
  • 🚫 Avoid keyword cannibalization β€” stop two of your own pages from competing for the same intent.
  • 🎯 Group by intent, not guesswork β€” clusters are based on real SERP overlap, not just similar wording.
  • πŸ“ˆ Prioritize by opportunity β€” each cluster rolls up total search volume so you tackle the biggest topics first.
  • βš™οΈ Tunable strictness β€” control how tightly keywords must overlap to be grouped.

▢️ Try it live

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.

Tip: lower common_num makes grouping looser; raise it to make clusters stricter.

πŸ”Ž View raw JSON response
Run the demo to see the response.

πŸ’‘ What just happened?

The 5 keywords above collapse into 2 clusters β€” because Google serves them with overlapping pages:

ClusterKeywordsTotal VolumeIntent
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.

πŸ“‹ Request Parameters

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 keyword
    • URLs β€” {"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).
    • true (hard): a keyword joins a cluster only if it overlaps with every keyword already in it β€” tight, high-precision groups.
    • false (soft): a keyword joins if it overlaps with any keyword in the cluster β€” larger, more inclusive groups.

πŸ“ Response Format

The response mirrors the input format β€” index-keyed columns, where each key is a row shared across every field:

  • πŸ“Œ Keyword β€” the keyword for that row
  • πŸ“„ Volume β€” its search volume
  • πŸ”— Cluster Name β€” which cluster it was assigned to (named after the cluster's first keyword)
  • πŸ”’ Number of Keywords in Cluster β€” how many keywords share that cluster
  • 🌐 URLs β€” the ranking URLs for that keyword (comma-separated)

Example response (from the demo above)

{
    "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"
    }
}

⚠️ Error Responses

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.