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 |