Opportunities

Opportunities

The opportunities report is the "what should I work on" entry point for a site — a ranked list of pages with GSC context, keyword opportunity, and the top open recommendations, backed by the same data the in-app opportunities view uses. It's read-only: fetching it never triggers a recompute, and there's no way to force one over the API — the underlying pipeline runs on its own schedule (crawl, keyword generation, scoring).

Scope: sites:read


GET /sites/{siteId}/opportunities

Path parameters

Parameter Type Description
siteId integer The site's ID

Request

curl -s https://new.metamonster.ai/api/v1/sites/42/opportunities \
  -H "Authorization: Bearer mm_YOUR_API_KEY"

Caching

Responses are cached per site for ~60 seconds. The X-Cache response header tells you whether you got a cached hit (HIT) or a fresh build (MISS) — useful if you're deciding whether to wait and re-poll versus trust what you got.

view_state and provisional scores

The report reflects a background pipeline (crawl → keyword generation → scoring) that may still be running:

view_state Meaning
setup Pipeline running, nothing rankable yet. opportunities may be empty.
generating Pipeline running; opportunities has provisional rows that can still change rank or score.
ready Nothing running — the ranking is final for the current data.

generation (object or null) gives finer-grained progress when a keyword-generation workflow is active: {workflow_id, total, completed, failed, pending, sealed}. sealed: false means pages are still streaming into the workflow and total will keep growing — don't treat total/completed as a stable denominator until sealed: true. generating_count is the number of pages excluded from the ranking because their keyword generation hasn't settled yet.

sealed: true only means total is now a stable denominator — pages already enrolled are still working through pending/completed/failed. It is not the done signal; view_state: "ready" is. Poll on view_state, not sealed.

If you need a fresh number, poll — re-GET this endpoint (respecting the ~60s cache) until view_state: "ready" and generation is null or sealed: true. There's no trigger endpoint; the pipeline can't be forced from here.

Subscription requirement

Unlike the in-app view (which shows a redacted teaser to non-subscribers), this endpoint requires an active subscription. A non-active org gets 402 payment_required with no partial data — there's no teaser mode over the API.

Not every page is ranked here

opportunities[] only ranks pages that are graded and have a keyword assigned — a page missing either isn't in the list at all, and the homepage is always excluded from opportunities (see below), even once graded and keyworded. Don't assume a row you see is representative of the whole site; check graded and recommendation_count on a row before assuming it's immediately actionable — graded: false means score/grade are null and there's nothing to act on yet.

score here is the opportunity score, not the content grade

An opportunity row's score/grade are the opportunity score (0–100, a demand/ease ranking signal — how much upside working this page has), not the page's SEO content grade. For the actual content score/grade, call GET /pages/{pageId}/analysis (overall_score/overall_grade) or read analysis.overall_score/overall_grade off the brief.

The gsc field has no avg_position

The top-level gsc object is {connected, clicks, impressions, ctr} — no avg_position. Average position lives one level down, per scope, at summary.scopes.top.now.avg_position and summary.scopes.all.now.avg_position (top = the pages shown in opportunities; all = every non-utility page on the site — logins, signup, privacy/terms and similar pages are excluded from both scopes. all.now itself is the site's undimensioned GSC totals, so it also picks up traffic to pages outside all's own page count, e.g. utility pages or URLs Google indexed that aren't tracked as pages).

Response 200

{
  "data": {
    "site": { "id": 42, "domain": "example.com", "domain_rating": 38.5, "gsc_property": "https://example.com/" },
    "view_state": "ready",
    "background_status": null,
    "generation": null,
    "generating_count": 0,
    "gsc": { "connected": true, "clicks": 1204, "impressions": 58900, "ctr": 0.0204 },
    "keyword_opportunity": { "addressable_volume": 42000, "potential_clicks": 3100, "keyword_count": 86 },
    "grade": { "average_score": 71.4, "average_grade": "B-", "graded_page_count": 40, "total_pending_recommendations": 112 },
    "summary": {
      "scopes": {
        "top": { "page_count": 20, "now": { "clicks": 900, "impressions": 41000, "ctr": 0.022, "avg_position": 14.2 }, "potential_volume": 18000 },
        "all": { "page_count": 40, "now": { "clicks": 1204, "impressions": 58900, "ctr": 0.0204, "avg_position": 16.8 }, "potential_volume": 42000 }
      },
      "shown_count": 20,
      "within_reach": { "keyword_count": 12, "volume_total": 9400 }
    },
    "homepage": null,
    "opportunities": [
      {
        "page_id": 5001,
        "rank": 1,
        "path": "/pricing",
        "url": "https://example.com/pricing",
        "title": "Pricing",
        "is_homepage": false,
        "graded": true,
        "score": 68,
        "grade": "C+",
        "primary_keyword": "example pricing",
        "volume": 2400,
        "difficulty": 32.5,
        "intent": "commercial",
        "clicks": 120,
        "impressions": 5400,
        "ctr": 0.0222,
        "position": 11.3,
        "reasons": [{ "category": "keyword_volume", "label": "High search volume, page not ranking top 10" }],
        "secondary_keywords": [{ "keyword": "example pricing plans", "volume": 320, "difficulty": 28 }],
        "recommendations": [
          { "id": 5500, "title": "Lead the title with the primary keyword", "impact": "medium", "target_field": "title", "why_it_matters": "Front-loaded keywords improve relevance signals." }
        ],
        "recommendation_count": 4
      }
    ],
    "recommendations": [
      {
        "id": 5500,
        "title": "Lead the title with the primary keyword",
        "impact": "medium",
        "target_field": "title",
        "page_id": 5001,
        "page_path": "/pricing",
        "primary_keyword": "example pricing",
        "keyword_volume": 2400,
        "impressions": 5400,
        "is_homepage": false,
        "is_priority": true
      }
    ]
  }
}

homepage is the homepage's own opportunity row (same shape as an opportunities[] entry) — it's surfaced separately because it's excluded from opportunities and ranked on its own. It's null when there's no page at the homepage path at all, or when that page is still mid keyword-generation (excluded the same way generating_count excludes rows from opportunities). Once the homepage page exists and isn't generating, homepage is always a row, even before it's been graded — an ungraded homepage comes back with graded: false, score: null, grade: null rather than homepage: null. recommendations is a top-level list of up to 8 cross-page recommendation highlights (distinct from the per-row opportunities[].recommendations), useful as a flat "start here" list without walking every page.

Errors

Status When
401 unauthorized Missing/invalid key
402 payment_required The organization has no active subscription
403 forbidden Key lacks sites:read
404 not_found No such site in your organization
429 rate_limited Rate limit exceeded