Content ideas
Preview. Content ideas are available to accounts in the content ideas preview. For any other key, every endpoint on this page answers 404 not_found, as if it did not exist. Whether a key has access follows the user who created it (or who authorized the OAuth connector).
MetaMonster studies a site's pages and live search data and writes a content playbook: the moves to make first, plays (themes, such as a page template crossed with a keyword family), and ideas, each a new page to create or an existing page to retitle, expand or optimize. A run also builds the Authority map: the site's core (what it is known for), the topics around it, and the pages drifting away from it. This is the same data as the dashboard's Ideas page.
Scope: sites:read for GET, content:write for POST.
The flow:
GET /sites/{siteId}/ideas/playbookfor the plan,GET /sites/{siteId}/ideasfor the ideas,GET /sites/{siteId}/ideas/topicsfor the Authority map.POST /sites/{siteId}/ideas/{ideaId}/acceptor/dismissto act on an idea.POST /sites/{siteId}/ideas/refreshto update the playbook (or start over), then pollGET /sites/{siteId}/ideas/run. A run takes about 10 minutes and has a real cost, so runs are limited (see below).
GET /sites/{siteId}/ideas
The site's ideas in one status, best first (playbook order).
| Query | Description |
|---|---|
status |
open (default), accepted (includes published) or dismissed. |
play |
A play id from the playbook, or none for ideas without an active play. |
topic |
A topic id from the Authority map, or none. |
funnel |
tofu (learning), mofu (comparing options) or bofu (ready to act or buy). |
change |
create, retitle, expand or optimize. |
q |
Matches the title and keywords. |
page, per_page |
Pagination (per_page 1-100, default 25). |
curl -s "https://new.metamonster.ai/api/v1/sites/42/ideas?funnel=bofu" \
-H "Authorization: Bearer mm_YOUR_API_KEY"
Response 200
{
"data": [
{
"id": "6f0c9a1e-2b7d-4c1a-9e55-0d3f1a2b3c4d",
"title": "Roof repair cost in Austin: 2026 price guide",
"status": "open",
"action": "create",
"change": "create",
"primary_keyword": "roof repair cost austin",
"secondary_keywords": ["roof leak repair cost"],
"volume": 390,
"difficulty": 18,
"cpc": 12.4,
"page_type": "service_page",
"funnel_stage": "bofu",
"why": "390 searches a month and the top results are national price lists with no local numbers.",
"caveats": ["Quote real local prices; a range without a source will not rank."],
"page_id": null,
"page_path": null,
"proposed_path": "/roof-repair-cost",
"play": { "id": "1b2c...", "name": "Local cost guides" },
"topic": { "id": "9d8e...", "name": "Roof repair" },
"score": 87.5,
"created_at": "2026-10-09T10:12:00Z",
"status_changed_at": null
}
],
"meta": { "total": 14, "page": 1, "per_page": 25 }
}
changeis what the idea does:createa new page, orretitle,expandoroptimizean existing one (page_id,page_path).actionis as stored:create,optimize, oreither(a page partly covers it, so it may be accepted either way; it reads ascreate).proposed_pathis the URL proposed for a new page; null when the idea has a page.whyis the evidence;caveatsare what to watch out for.
GET /sites/{siteId}/ideas/{ideaId}
One idea in any status, in full: the fields above plus description, keywords (the measured keywords behind it, each { keyword, volume, kd, cpc }), accepted_action, accepted_keywords, dismiss_reason and dismiss_note.
GET /sites/{siteId}/ideas/playbook
The latest playbook, or { "data": null } before the first successful run.
{
"data": {
"id": "c1d2...",
"created_at": "2026-10-09T10:12:00Z",
"moves": [
{ "title": "Publish the Austin cost guide", "why": "...", "effort": "S", "searches_per_month": 390, "keywords": ["roof repair cost austin"], "idea_ids": ["6f0c9a1e-..."] }
],
"plays": [
{
"id": "1b2c...", "name": "Local cost guides", "pattern": "{service} cost in {city}", "rationale": "...",
"effort": "M", "channels": [], "notes": [], "rank": 0, "volume": 1240,
"ideas": [{ "id": "6f0c9a1e-...", "title": "...", "primary_keyword": "roof repair cost austin", "status": "open", "change": "create", "page_path": null, "proposed_path": "/roof-repair-cost" }]
}
],
"keywords": [{ "keyword": "roof repair cost austin", "play": "Local cost guides", "volume": 390, "kd": 18, "cpc": 12.4, "idea_id": "6f0c9a1e-..." }],
"notes": [{ "kind": "skipped", "text": "Left out DIY repair guides: the results are video-led." }],
"mode": "update",
"changes": ["Kept the cost guides and moved Austin to the top.", "Added a play for storm damage."],
"needs_live_update": false
}
}
How it was written:
| Field | Meaning |
|---|---|
mode |
update: written from the playbook before it. rebuild: written from scratch. |
changes |
What an update changed, as plain sentences. Empty for a rebuild. |
needs_live_update |
true when the playbook was written before the site went live and the site has been crawled since. Start an update (POST /sites/{siteId}/ideas/refresh) to check the plan against the real pages and search data. |
notes are about the whole playbook: data (limits of the data) and skipped (ideas measured and left out, with why). Warnings about one page are on that idea's caveats.
GET /sites/{siteId}/ideas/topics
The Authority map as data, without drawing coordinates.
{
"data": {
"core": { "label": "Residential roofing in Austin", "summary": "..." },
"topics": [
{ "id": "9d8e...", "name": "Roof repair", "description": "...", "core_distance": 0.1, "distance": "core", "page_count": 6, "drifting_count": 0, "idea_count": 4, "gap": false, "edited": false }
],
"drifting_pages": [{ "page_id": 311, "path": "/careers", "title": "Careers", "topic": null, "reason": "No topic fits this page well" }],
"cross_topic_pages": [{ "page_id": 305, "path": "/storm-damage", "title": null, "topics": [{ "id": "9d8e...", "name": "Roof repair" }, { "id": "7a6b...", "name": "Insurance claims" }] }]
}
}
core_distance runs from 0 (the core itself) to 1 (unrelated to what the site is known for); distance is its band (core under 0.25, close under 0.5, adjacent under 0.75, else distant). A topic with gap: true has ideas but no pages yet. core is null and the lists are empty before a run has built the map; planned sites have none.
POST /sites/{siteId}/ideas/refresh
Start a run. By default a site that already has a playbook is updated: the run keeps the plan and adjusts it to the latest data, and the new playbook says what changed (changes). A site without a playbook gets a new one. Send mode: "rebuild" to start over and write a new playbook from scratch; open ideas the new playbook leaves out are removed, and accepted and published ideas stay. Use it when the business or the site has changed a lot.
Each run has a real cost (paid research and search data), and both modes use one run, so the same limits as the dashboard apply:
| Status | Code | When |
|---|---|---|
402 |
payment_required |
No active subscription. |
400 |
invalid_request |
The site has no crawl and no planned pages yet, the body is not valid JSON, mode is not update or rebuild, or the body has another key. |
409 |
ideas_run_in_progress |
A run is already in progress (details.run_id). |
429 |
ideas_refresh_cooldown |
The site had a run in the last 24 hours (details.retry_at). |
429 |
ideas_run_cap_reached |
The organization used this month's runs. |
Request body (optional)
{ "mode": "update" }
| Field | Type | Notes |
|---|---|---|
mode |
"update" or "rebuild" |
Optional. No body, or no mode, means update. Any other value, a body that is not valid JSON, or any other key is a 400 invalid_request. |
Response 202
{ "data": { "status": "queued" } }
The run takes about 10 minutes. Its id appears on GET /sites/{siteId}/ideas/run once it starts, usually within seconds.
GET /sites/{siteId}/ideas/run
The latest run, or { "data": null } before the first. Spends the jobs rate bucket, so it is safe to poll (every minute or two is plenty).
{
"data": {
"id": "e5f6...", "status": "running", "stale": false, "trigger": "manual",
"stage": "research", "progress": "Measuring 40 keywords", "mode": "update",
"started_at": "2026-10-09T10:02:00Z", "finished_at": null,
"ideas": 14, "last_succeeded_at": "2026-09-09T10:11:00Z",
"can_refresh_at": "2026-10-10T10:02:00Z", "auto_refresh": false, "next_refresh_at": null
}
}
status is running, succeeded or failed; a run that stopped reporting reads as failed with stale: true. stage is understand, research, validate or write. mode says how the run writes the playbook: update adjusts the current one, rebuild writes it from scratch. It is null for the first seconds of a run, until the worker has settled on one, and for runs from before update mode. ideas and last_succeeded_at describe the last successful run.
POST /sites/{siteId}/ideas/{ideaId}/accept
open → accepted, like Accept in the dashboard. The action must fit the idea (400 otherwise); 409 when the idea is not open.
| Body | For | Effect |
|---|---|---|
{ "action": "create", "page_id": 512 } |
create and either ideas |
Records the page made for the idea. Create it first with POST /sites/{siteId}/pages (see Pages). |
{ "action": "optimize" } |
optimize and either ideas |
Adds the idea's keywords to its page's secondary keywords (deduped, 10 at most) and starts a forced analysis so the recommendations target them. The analysis spends one unit of the plan's analysis allowance. |
Response 200
{
"data": {
"page_id": 204,
"added_keywords": ["roof leak repair cost"],
"analysis_started": true,
"analysis_reason": null,
"idea": { "id": "...", "status": "accepted", "...": "..." }
}
}
An analysis that can't start does not undo the accept: analysis_reason is plan_required, already_running or error. Nothing is added (and no analysis starts) when the page already targets every keyword.
POST /sites/{siteId}/ideas/{ideaId}/dismiss
open → dismissed. Body: { "reason": "not_relevant" | "already_covered" | "not_now" | "wont_rank" | "other", "note"?: string } (note up to 500 chars). Later runs do not suggest the idea's keywords again. Response 200: { "data": ContentIdeaDetail }. 409 when the idea is not open.