List content ideas
GET
/sites/{siteId}/competitors/content-ideas- Scope
search:read- MCP tool
content_ideas
Content ideas: keyword gaps grouped by topic The keyword gaps (competitors rank, we do not; off-topic and competitor-brand keywords left out) grouped by topic, most opportunity first; topic null holds keywords not reviewed yet. suggestion: new_page (no page of ours ranks in the topic), expand (ourPages already rank in it) or review. Reads the stored monthly gap refresh: no new lookups. Also accepted in snake_case (min_volume for minVolume).
Parameters
| Name | In | Details |
|---|---|---|
siteIdRequired | path | stringWebsite id, from GET /sites. |
domains | query | stringComma-separated competitor domains, each in the set. Default: the competitors that drive gaps. |
minVolume | query | integermin 0, max 10000000Only keywords with at least this many monthly searches. Also accepted in snake_case (min_volume for minVolume). |
maxDifficulty | query | integermin 0, max 100Only keywords at most this hard (0–100; 0 = no limit). |
keywords | query | integerdefault 5, min 1, max 50Top keywords listed per topic. |
page | query | integerdefault 1, min 1Page number, from 1. |
per | query | integerdefault 20, min 1, max 200Topics per page, 1 to 200. |
Response
200 · application/json
| Field | Details |
|---|---|
domains | list of string · required; The competitors compared |
total | integer · required; Rows that match, over all pages |
page | integer · required; This page, from 1 |
per | integer · required; Rows per page |
hasMore | true or false · required; More rows after this page |
gapKeywords | integer · required; Gap keywords over all topics |
hiddenKeywords | integer · required; Off-topic and competitor-brand keywords left out |
topics | list of object · required |
topics[].topic | string or null · required; The topic; null: keywords not reviewed yet |
topics[].keywords | integer · required; Gap keywords in the topic |
topics[].volume | integer · required; Monthly searches summed (estimate) |
topics[].opportunity | integer · required; Sum of the keywords' opportunity |
topics[].bestOpportunity | integer · required; The highest keyword opportunity, 0–100 |
topics[].avgDifficulty | integer or null · required; Average difficulty, 0–100 |
topics[].suggestion | new_page | expand | review · required; What to do |
topics[].ourPages | list of object · required; Our pages already ranking in the topic (at most 5) |
topics[].ourPages[].url | string · required; Page address |
topics[].ourPages[].path | string · required; Path of the address |
topics[].ourPages[].keywords | integer · required; Keywords of the topic it ranks for |
topics[].competitorPages | list of object · required; Competitor pages ranking for most of the topic (at most 5) |
topics[].competitorPages[].url | string · required; Page address |
topics[].competitorPages[].path | string · required; Path of the address |
topics[].competitorPages[].keywords | integer · required; Keywords of the topic it ranks for |
topics[].top | list of object · required; The best keywords of the topic |
topics[].top[].keyword | string · required; The search phrase |
topics[].top[].volume | integer or null · required; Monthly searches (estimate) |
topics[].top[].difficulty | integer or null · required; How hard it is to rank, 0–100 |
topics[].top[].opportunity | integer · required; 0–100 |
topics[].top[].relevance | integer or null · required; Fit with the website, 0–100 |
topics[].top[].positions | object · required; Each competitor's Google position, by domain |
topics[].top[].positions.bikeworld.example | number |
topics[].top[].positions.cyclehub.example | number |
topics[].top[].pages | object · required; Each competitor's ranking page, by domain |
topics[].top[].pages.bikeworld.example | string |
topics[].top[].pages.cyclehub.example | |
note | string · required; How to read the numbers |
link | address (URL) · required; The Competitors screen in the app |
Errors
| Status | Meaning |
|---|---|
400 | Bad request. Codes: unknown_competitor, bad_keywords, bad_minVolume, bad_maxDifficulty, bad_page, bad_per. |
401 | Missing, unknown or revoked key. Codes: missing_key, invalid_key. |
403 | The key lacks the scope, covers another website, or its owner lost access. Codes: missing_scope, other_site, owner_left, no_owner, forbidden. |
404 | Not found. Codes: not_found. |
429 | Too many requests this hour for this key. Codes: rate_limited. |
Example request
curl \
-H "Authorization: Bearer mr_ws_…" \
"https://app.monoranks.com/api/v1/sites/SITE_ID/competitors/content-ideas"const res = await fetch("https://app.monoranks.com/api/v1/sites/SITE_ID/competitors/content-ideas", {
headers: { Authorization: "Bearer mr_ws_…" },
});
if (!res.ok) throw new Error((await res.json()).error.message);
const data = await res.json();import requests
res = requests.get(
"https://app.monoranks.com/api/v1/sites/SITE_ID/competitors/content-ideas",
headers={"Authorization": "Bearer mr_ws_…"},
)
res.raise_for_status()
data = res.json()<?php
$ch = curl_init("https://app.monoranks.com/api/v1/sites/SITE_ID/competitors/content-ideas");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer mr_ws_…"],
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);import axios from "axios";
const { data } = await axios.get("https://app.monoranks.com/api/v1/sites/SITE_ID/competitors/content-ideas", { headers: { Authorization: "Bearer mr_ws_…" } });Example response
{
"domains": [
"bikeworld.example",
"cyclehub.example"
],
"total": 12,
"page": 1,
"per": 20,
"hasMore": false,
"gapKeywords": 318,
"hiddenKeywords": 23,
"topics": [
{
"topic": "Tires",
"keywords": 24,
"volume": 18400,
"opportunity": 1210,
"bestOpportunity": 74,
"avgDifficulty": 31,
"suggestion": "expand",
"ourPages": [
{
"url": "https://acmebikes.com/tires/gravel/",
"path": "/tires/gravel/",
"keywords": 6
}
],
"competitorPages": [
{
"url": "https://bikeworld.example/tires/tubeless/",
"path": "/tires/tubeless/",
"keywords": 9
}
],
"top": [
{
"keyword": "tubeless gravel tires",
"volume": 1900,
"difficulty": 28,
"opportunity": 74,
"relevance": 90,
"positions": {
"bikeworld.example": 4,
"cyclehub.example": 9
},
"pages": {
"bikeworld.example": "https://bikeworld.example/tires/tubeless/",
"cyclehub.example": null
}
}
]
}
],
"note": "Keyword gaps grouped by topic, most opportunity first.",
"link": "https://app.monoranks.com/sites/3f6c2a1e-8b4d-4c7a-9e21-5d0b7a6f4c13/competitors"
}{
"error": {
"code": "unknown_competitor",
"message": "Not in the competitor set: shop.example. GET /competitors lists it."
}
}{
"error": {
"code": "missing_key",
"message": "Send the key as Authorization: Bearer mr_site_…"
}
}{
"error": {
"code": "missing_scope",
"message": "This key does not have the scope this call needs."
}
}{
"error": {
"code": "not_found",
"message": "Website not found."
}
}{
"error": {
"code": "rate_limited",
"message": "More than 1200 requests this hour for this key."
}
}The MCP server does the same with the tool content_ideas.