Content ideas

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:

  1. GET /sites/{siteId}/ideas/playbook for the plan, GET /sites/{siteId}/ideas for the ideas, GET /sites/{siteId}/ideas/topics for the Authority map.
  2. POST /sites/{siteId}/ideas/{ideaId}/accept or /dismiss to act on an idea.
  3. POST /sites/{siteId}/ideas/refresh to update the playbook (or start over), then poll GET /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 }
}
  • change is what the idea does: create a new page, or retitle, expand or optimize an existing one (page_id, page_path).
  • action is as stored: create, optimize, or either (a page partly covers it, so it may be accepted either way; it reads as create).
  • proposed_path is the URL proposed for a new page; null when the idea has a page.
  • why is the evidence; caveats are 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.