Topic Clusters API

Samuel Schmitt

Looking for an API that returns your topic clusters and the SERP data behind them? With the thruuu Topic Clusters API, you can pull:

If you plan content in clusters and want the cluster data in your own stack, you’re in the right place.

Getting started with the thruuu Topic Clusters API

The Topic Clusters API gives you programmatic access to your thruuu topic cluster projects. Use it to pull your clusters into your own tools, dashboards or AI agents, and find out:

Your first call

All requests go to https://api.thruuu.com and include your API key in the header:

curl https://api.thruuu.com/api/v2/topic-clusters \
  -H "Authorization: Bearer YOUR_API_KEY"

This returns your projects. From there, open a project to list its clusters, and a cluster to see its keywords, the search results and the questions people ask.

Credits

Reading your data is free. The only call that costs credits is analysing a cluster’s pages (1 credit per cluster), and you’re only charged when the analysis succeeds.

Note: The API is available on the Professional and Agency plans. If you don’t yet have an account and API key, please read this page first.

Concepts

A project is one Topic Clusters analysis: a keyword list, grouped into clusters for one location, language and device. A project optionally has your domain configured, which is what turns on ranking data.

A cluster is a group of keywords that share enough of the same Google results to be targeted by one page. Each cluster has a main keyword, a combined Mixed SERP (the results that appear across its keywords), and the People Also Ask questions and related searches collected from those keywords.

A cluster can be scraped. Scraping visits every page in the cluster’s Mixed SERP and extracts its content: headings, word count, images, FAQ, schema types, body text. It costs 1 credit per cluster and runs in the background. Until a cluster is scraped, its SERP is available but the page content is not.

Ranking fields only exist when your domain is configured. On a project with no domain, the three ranking fields on cluster rows are left out entirely, not sent as null.

Projects are created in the app: building a Topic Clusters covers the keyword list you upload and the SERP settings that go with it.

Endpoints

EndpointsDescription
GET /api/v2/topic-clustersyour projects
GET /api/v2/topic-clusters/{id}one project
GET /api/v2/topic-clusters/{id}/clustersthe project’s clusters, paginated
GET /api/v2/topic-clusters/{id}/clusters/{clusterId}one cluster, with its full SERP and page content
GET /api/v2/topic-clusters/{id}/clusters/{clusterId}/paaone cluster’s People Also Ask questions
GET /api/v2/topic-clusters/{id}/clusters/{clusterId}/related-searchesone cluster’s related searches
GET /api/v2/topic-clusters/{id}/domainscompeting domains across the project, paginated
POST /api/v2/topic-clusters/{id}/clusters/{clusterId}/scrapescrape a cluster, 1 credit

1. List projects

GET /api/v2/topic-clusters
ParameterDefaultNotes
page1
itemsPerPage10Maximum 100. A higher value is reduced to 100 rather than rejected.
workspace_idRestrict to one workspace.

Newest project first. An API key sees every project its account can see in the app, including projects owned by teammates on a company account.

{
  "items": [
    {
      "id": "6a1f3c9e2b7d4e0012a4c581",
      "label": "Patent costs UK",
      "status": "Done",
      "domain": "example.com",
      "pageRank": 38,
      "searchParam": {
        "location": null, "country": "GB", "language": "en",
        "search_engine": "google.co.uk", "device": "desktop", "urlOverlap": 4
      },
      "clusterCount": 184,
      "keywordCount": 1250,
      "createdAt": "2026-09-12T08:41:07.512Z"
    }
  ],
  "total": 14,
  "page": 1,
  "itemsPerPage": 10
}
FieldMeaning
idThe project id, used as {id} on every other endpoint.
labelThe project name.
statusInit (not processed yet), In Progress, or Done.
domainYour domain, or null if none is configured. When null, cluster rows carry no ranking fields.
pageRankYour domain’s PageRank on a 0 to 100 scale, or null when no domain is configured or no rank was computed.
searchParamThe search configuration the project was analysed under: location, country, language, search engine, device. urlOverlap is how many shared results two keywords need to join the same cluster, or null if the default was used.
clusterCountClusters in the project.
keywordCountKeywords the project was created with.
createdAtWhen the project was created.

2. Get a project

GET /api/v2/topic-clusters/{id}

The same object as one row of endpoint 1, fetched by id. No parameters.

{
  "id": "6a1f3c9e2b7d4e0012a4c581",
  "label": "Patent costs UK",
  "status": "Done",
  "domain": "example.com",
  "pageRank": 38,
  "searchParam": {
    "location": null, "country": "GB", "language": "en",
    "search_engine": "google.co.uk", "device": "desktop", "urlOverlap": 4
  },
  "clusterCount": 184,
  "keywordCount": 1250,
  "createdAt": "2026-09-12T08:41:07.512Z"
}

3. List clusters

GET /api/v2/topic-clusters/{id}/clusters

The project’s clusters, as the app’s cluster cards show them. Rows come back in the project’s stored cluster order, which does not change between calls, so paging through is safe.

ParameterDefaultNotes
page1
itemsPerPage40Maximum 150. A higher value is reduced to 150 rather than rejected.
{
  "items": [
    {
      "id": "6a1f3d4b2b7d4e0012a4c5f0",
      "mainKw": "how much does a patent cost uk",
      "mainKwVolume": 880,
      "count": 12,
      "category": "Patent Costs in the UK",
      "intent": "informational",
      "averagePR": 41,
      "serpFeatures": [
        { "feature": "paa", "visibility": 100 },
        { "feature": "related searches", "visibility": 100 },
        { "feature": "aio", "visibility": 92 }
      ],
      "similarity": [
        "how much does a patent cost uk",
        "cost to file a patent uk",
        "uk patent application fee"
      ],
      "hidden": false,
      "isFavorite": false,
      "scraped": false,
      "volume": 2340,
      "briefId": null,
      "isRanking": true,
      "avgPosition": 8.4,
      "bestPageUrl": "https://example.com/patent-costs"
    }
  ],
  "total": 184,
  "page": 1,
  "itemsPerPage": 40
}
FieldMeaning
idThe cluster id, used as {clusterId}. Can be null in the rare case the cluster record was never created; such a cluster cannot be fetched or scraped.
mainKwThe cluster’s main keyword.
mainKwVolumeMonthly search volume of the main keyword alone, or null.
countNumber of keywords in the cluster.
categoryThe cluster’s category, or null if uncategorised.
intentSearch intent, such as informational or commercial, or null if not classified.
averagePRAverage PageRank (0 to 100) of the domains in the cluster’s SERP. A measure of how strong the competition is.
serpFeaturesSERP features present across the cluster’s keywords. visibility is the percentage of keywords showing that feature. Features with zero visibility are left out.
similarityEvery keyword in the cluster, main keyword included.
hiddenWhether the cluster is hidden in the app. Always a boolean.
isFavoriteWhether the cluster is marked as a favourite in the app. Always a boolean.
scrapedWhether the cluster’s page content has been scraped (see endpoint 8).
volumeTotal monthly search volume across all the cluster’s keywords, or null. Not the same as mainKwVolume.
briefIdThe id of the content brief created from this cluster, or null.
isRankingOnly when the project has a domain. Whether your domain ranks for this cluster.
avgPositionOnly when the project has a domain. Your average organic position across the cluster’s keywords, or null.
bestPageUrlOnly when the project has a domain. Your best ranking page for the cluster, or null.

The three ranking fields are present or absent together, on every row, depending on the project’s domain. Check domain once on the project rather than testing each row.

4. Get a cluster

GET /api/v2/topic-clusters/{id}/clusters/{clusterId}

One cluster in full: its keywords, and its Mixed SERP with every result, People Also Ask question and related search. Once the cluster is scraped, each SERP result also carries the page’s content. No parameters.

Size depends on whether the cluster has been scraped. Measured on real clusters: about 30 KB unscraped, about 750 KB scraped, and up to about 2 MB for the largest. Most of a scraped response is body, kwBody and semanticBody.

{
  "id": "6a1f3d4b2b7d4e0012a4c5f0",
  "mainKw": "how much does a patent cost uk",
  "mainKwVolume": 880,
  "keywords": [
    { "keyword": "how much does a patent cost uk", "volume": 880 },
    { "keyword": "cost to file a patent uk", "volume": 320 }
  ],
  "intent": "informational",
  "count": 12,
  "volume": 2340,
  "category": "Patent Costs in the UK",
  "hidden": false,
  "isFavorite": false,
  "scraped": true,
  "mixedSerp": {
    "paa": [
      {
        "question": "How much does it cost to get a patent in the UK?",
        "answer": "The cost of a UK patent depends on the fees and professional help involved...",
        "source": { "link": "https://www.gov.uk/patent-your-invention", "displayed_link": "gov.uk", "title": "Patenting your invention" }
      }
    ],
    "related_searches": [
      { "title": "uk patent renewal fees", "url": "https://www.google.co.uk/search?q=uk+patent+renewal+fees" }
    ],
    "result": [
      {
        "position": 1,
        "domain": "gov.uk",
        "serp_type": "page",
        "serp_title": "Patenting your invention: What you can patent",
        "serp_description": "You can use a patent to protect your invention...",
        "url": "https://www.gov.uk/patent-your-invention",
        "type": "article",
        "canonical": "https://www.gov.uk/patent-your-invention",
        "description": "How to apply for a patent, costs and timescales.",
        "h1": "Patenting your invention",
        "h2": ["What you can patent", "How much it costs"],
        "h3": ["Filing online"],
        "published_time": { "lastUpdate": "2 years ago", "dateFormatted": "06 December 2023", "dateISO": "2023-12-06T13:44:39+01:00" },
        "ogType": "article",
        "wordCount": 1480,
        "imgCount": 3,
        "images": [{ "src": "https://www.gov.uk/images/patent.png", "alt": "Patent process" }],
        "videos": [],
        "lang": "en",
        "faq_on_page": [],
        "anchors": { "size": 42, "outboundSize": 5, "list": [{ "text": "Apply online", "href": "https://www.gov.uk/apply", "hrefDomain": "gov.uk", "rel": "", "isOutbound": false }] },
        "toc": [{ "id": 0, "level": "1", "name": "What you can patent", "tag": "h2", "children": [] }],
        "og": { "ogImage": "https://www.gov.uk/images/og.png" },
        "schema_type": ["WebPage", "BreadcrumbList"],
        "comment_questions": [],
        "body": "Patenting your invention. You can use a patent to protect your invention...",
        "kwBody": [{ "term": "patent", "tf": 49, "n": 1 }, { "term": "patent application", "tf": 12, "n": 2 }],
        "semanticBody": [{ "tag": "h1", "text": "Patenting your invention", "type": "heading", "url": null, "index": 0, "length": 24 }],
        "pageRank": 91
      }
    ]
  },
  "createdAt": "2026-09-12T08:52:13.004Z",
  "updatedAt": "2026-09-20T14:03:51.887Z"
}
FieldMeaning
idThe cluster id, same as {clusterId}.
mainKwThe cluster’s main keyword.
mainKwVolumeMonthly search volume of the main keyword, or null.
keywordsEvery keyword in the cluster, as { keyword, volume } objects.
intentSearch intent, such as informational, or null if not classified.
countNumber of keywords in the cluster.
volumeTotal monthly search volume across the cluster’s keywords, or null.
categoryThe cluster’s category, or null.
hiddenWhether the cluster is hidden in the app.
isFavoriteWhether the cluster is a favourite in the app.
scrapedWhether page content has been scraped. Same value as scraped on endpoint 3.
mixedSerpThe cluster’s combined SERP, described below. null if the cluster has none.
createdAtWhen the cluster was created.
updatedAtWhen the cluster last changed. A completed scrape moves this forward.

mixedSerp

FieldMeaning
paaPeople Also Ask questions. Each entry has question, and when Google supplied them answer, answer_list (a list-style answer, as [{ text }]), source ({ link, displayed_link, title }) and ai_overview (an AI Overview attached to that question, with ai_overview_contents and ai_overview_sources).
related_searchesRelated searches, each { title, url }.
resultThe SERP results, one entry per result.

For question counts and a lighter response, use endpoints 5 and 6 instead.

result[] entries

SERP fields, present whether or not the cluster is scraped:

FieldMeaning
positionPosition in the Mixed SERP.
date, date_utcThe date Google shows next to the result, as displayed and as ISO. Only on results that show one.
domainThe result’s domain.
serp_typeThe kind of result, for example page (organic) or featured snippet.
serp_titleThe title shown in Google.
serp_descriptionThe snippet shown in Google.
urlThe result’s URL.
pageRankThe domain’s PageRank, 0 to 100.

Page content fields, only on a scraped cluster:

FieldMeaning
typePage type, for example article.
canonicalCanonical URL.
descriptionMeta description.
h1The H1 heading.
h2, h3Arrays of H2 and H3 headings, in page order.
published_time, modified_time{ lastUpdate, dateFormatted, dateISO }, when the page states them.
ogTypeOpen Graph type.
wordCountWords in the main content.
imgCountImages in the main content.
images[{ src, alt }].
videosEmbedded videos, [{ src, videoTitle, ytid }].
langDeclared page language, or null.
faq_on_pageFAQ entries found on the page, [{ index, question, answer }].
anchorsLinks on the page: { size, outboundSize, list: [{ text, href, hrefDomain, rel, isOutbound }] }.
tocThe heading outline as a tree: [{ id, level, name, tag, children }].
ogOpen Graph data, such as ogImage.
schema_typeSchema.org types found on the page.
comment_questionsQuestions found in the page’s comments.
bodyThe main content as plain text.
kwBodyMost frequent terms in the content: [{ term, tf, n }], where tf is the count and n the number of words in the term.
semanticBodyThe content as ordered blocks: [{ tag, text, type, url, index, length }].

Absent, not null. A field with no value on the stored result is left out of that entry. On an unscraped cluster, every page content field is absent from every result, so test for the key ("wordCount" in result) or use scraped, never result.wordCount === null.

5. People Also Ask questions

GET /api/v2/topic-clusters/{id}/clusters/{clusterId}/paa

The cluster’s People Also Ask questions, each with how many of the cluster’s keywords surfaced it. The quickest way to see which questions matter most for a cluster. No parameters.

This data is collected when the project is created, so it is available on every cluster, scraped or not.

{
  "id": "6a1f3d4b2b7d4e0012a4c5f0",
  "mainKw": "how much does a patent cost uk",
  "paa": [
    { "question": "How much does it cost to get a patent in the UK?", "answer": "The cost of a UK patent depends on the fees and professional help involved...", "count": 9 },
    { "question": "Is it worth patenting an idea?", "answer": null, "count": 4 }
  ]
}
FieldMeaning
questionThe question.
answerGoogle’s answer snippet, or null when none was captured.
countHow many keywords in the cluster showed this question.

Sorted by count, highest first. Not paginated: the full list comes back in one response. A cluster with no questions returns "paa": [], not an error.

GET /api/v2/topic-clusters/{id}/clusters/{clusterId}/related-searches

The cluster’s related searches, each with how many of the cluster’s keywords surfaced it. No parameters. Like endpoint 5, available on every cluster, scraped or not.

{
  "id": "6a1f3d4b2b7d4e0012a4c5f0",
  "mainKw": "how much does a patent cost uk",
  "relatedSearches": [
    { "title": "uk patent renewal fees", "url": "https://www.google.co.uk/search?q=uk+patent+renewal+fees", "count": 7 },
    { "title": "patent attorney cost uk", "url": null, "count": 3 }
  ]
}
FieldMeaning
titleThe related search.
urlGoogle’s link for that search, or null.
countHow many keywords in the cluster showed this related search.

Sorted by count, highest first. Not paginated. A cluster with none returns "relatedSearches": [].

7. Competing domains

GET /api/v2/topic-clusters/{id}/domains

Every domain that appears in the project’s clusters, with how often. Sorted by count, highest first.

ParameterDefaultNotes
page1
itemsPerPage20Maximum 100. A higher value is reduced to 100 rather than rejected.
{
  "items": [
    { "hostname": "gov.uk", "count": 77, "countSERP": 375, "pageRank": 91 },
    { "hostname": "legalzoom.com", "count": 48, "countSERP": 167, "pageRank": 62 }
  ],
  "total": 312,
  "page": 1,
  "itemsPerPage": 20
}
FieldMeaning
hostnameThe domain.
countNumber of clusters whose Mixed SERP includes this domain.
countSERPTotal appearances across the SERPs of all the project’s keywords.
pageRankThe domain’s PageRank, 0 to 100, or null.

A project with no domain data returns "items": [] and "total": 0.

8. Scrape a cluster

POST /api/v2/topic-clusters/{id}/clusters/{clusterId}/scrape

Scrapes the page content of every result in the cluster’s Mixed SERP, the same as clicking Analyze on a cluster in the app. No request body.

Costs 1 credit, charged only when the scrape succeeds. The request itself never charges, which is why every response says "charged": false. The scrape runs in the background; if it fails, you are not charged.

HTTPstatusMeaning
202scrapingThe scrape has started.
200already_scrapedThe cluster is already scraped. Nothing happens, nothing is charged.
200insufficient_creditsNot enough credits. Nothing happens.
503scrape_service_unavailableThe scrape service could not be reached. Nothing was charged.
{ "status": "scraping", "charged": false }
{
  "status": "insufficient_credits",
  "charged": false,
  "message": "Insufficient credits to scrape this cluster. This action requires 1 credit; your account has 0 available."
}
{
  "status": "scrape_service_unavailable",
  "charged": false,
  "message": "The scrape service is temporarily unavailable. No credit was charged. Try again shortly."
}

Knowing when it is done. Poll endpoint 4 (or endpoint 3) until scraped is true. On endpoint 4, updatedAt also moves forward when the scrape completes.

Do not trigger the same cluster twice while it is running. The already_scraped check only applies once a scrape has finished. Two requests made while the first scrape is still in progress start two scrapes, and both are charged. Trigger once, then poll.

Rate limit

100 requests per 10 seconds. Over the limit you get 429 with:

{ "error": "Too many attempts, please try again shortly." }

Standard RateLimit-* headers are returned on every response.

Errors

StatusWhen
400A malformed id, or an invalid query parameter on endpoints 1, 3 and 7.
401Missing or invalid API key.
403Your plan does not include API access to Topic Clusters.
404The project or cluster does not exist, or is not yours.
429Rate limit exceeded.
503Endpoint 8 only: the scrape service is unavailable.
{ "message": "Topic cluster not found" }

A project that does not exist and a project belonging to another account return the same 404, so project ids cannot be probed. The same applies to clusters, including a clusterId that belongs to a different project than {id}.

StatusMessageMeaning
400Invalid id.{id} or {clusterId} is not a valid id.
400Invalid workspace_id.workspace_id is not a valid id.
400Payload validation errorA bad query parameter. See below.
401Authorization token is not suppliedNo API key sent.
401Authorization token is not valid.The key is wrong or revoked.
403Topic Clusters API access requires a Professional or Agency plan.Plan gate. The body also carries "error": "plan_required".
404Topic cluster not foundNo such project, or not yours.
404Cluster not foundNo such cluster in this project, or not yours.

Endpoints 1, 3 and 7 validate their query string strictly. A non-integer or zero page or itemsPerPage, or any parameter they do not accept, returns 400 with the offending parameters listed:

{
  "validationErrors": { "include": "\"include\" is not allowed" },
  "message": "Payload validation error"
}

An empty parameter value (?page=) is treated as if the parameter were absent.

A content-gap pull, end to end

1. Find the project.

GET /api/v2/topic-clusters

Note its id, and whether domain is set: that tells you whether cluster rows will carry ranking fields.

2. Page through the clusters.

GET /api/v2/topic-clusters/{id}/clusters?itemsPerPage=150

Keep requesting the next page until you have total rows. Your content gaps are the rows with isRanking: false, and volume tells you which are worth the most. Rows with a briefId already have a brief.

3. Get the questions for a gap.

GET /api/v2/topic-clusters/{id}/clusters/{clusterId}/paa

The top of the list, by count, is what the page targeting that cluster needs to answer. Free, and available without scraping.

4. Scrape it, if you need the competitors’ content.

POST /api/v2/topic-clusters/{id}/clusters/{clusterId}/scrape

Then poll until scraped is true:

GET /api/v2/topic-clusters/{id}/clusters/{clusterId}

Each mixedSerp.result[] entry now carries the ranking page’s headings, word count, FAQ and body.

Notes for automated pulls

Topic Clusters is one of several APIs on the same key, and the overview of the thruuu API lists the rest.

Get started with the thruuu Topic Clusters API

Group your keywords into the pages they should actually become, and see who already ranks for each one.
thruuu builds the clusters, and the API moves them into whatever system you plan in.