Brief
The brief is a page's work packet — everything needed to start working on a page, in one call, instead of five or six. Start here when an agent picks a page to work on (e.g. from opportunities): it saves the round-trips of separately fetching the page, its content, its analysis, its keywords, its GSC queries, its outline, and its links.
Scope: sites:read
GET /pages/{pageId}/brief
Path parameters
| Parameter | Type | Description |
|---|---|---|
pageId |
integer | The page's ID |
Request
curl -s https://new.metamonster.ai/api/v1/pages/5001/brief \
-H "Authorization: Bearer mm_YOUR_API_KEY"
Sections degrade independently
The page itself is the only hard dependency — 404 if it doesn't exist or isn't yours. Every other section is fetched in parallel and degrades on its own failure rather than failing the whole call: a GSC outage shows up as queries.gsc_status: "unavailable", a page with no analysis yet shows analysis: null, and so on. Never assume a 200 means every section succeeded — check each section's own status/null-ness.
| Section | What it is | Same data standalone at | How it degrades |
|---|---|---|---|
page |
Page summary (id, url, path, title, primary keyword, priority, opportunity score). | GET /pages/{pageId} (fuller detail) |
N/A — required; the brief 404s if the page doesn't resolve. |
fields |
Resolved current value + status for title, meta_description, h1, schema, body — draft if one exists, else the live/snapshot value. fields.schema also carries validation (JSON-LD check against the saved value); fields.body also carries content_version_id. |
GET /pages/{pageId}/drafts, GET /pages/{pageId}/content |
On failure, all five fields are null. |
analysis |
Latest non-skipped analysis with its components, estimated_score, estimated_grade, or null. |
GET /pages/{pageId}/analysis |
null if there's no analysis yet, or on a load failure — you can't tell those apart from the brief alone; if it matters, call /analysis directly. |
recommendations |
Open recommendations only (pending/in_progress — no applied/dismissed history), each with content_excerpt. |
GET /pages/{pageId}/analysis (recommendations, includes closed ones too) |
Empty array on failure or when there's no analysis. |
keywords |
primary and secondary keywords with cached metrics (search volume, difficulty, intent), or null metrics on a cache miss per keyword. |
GET /keywords/metrics (per keyword) |
Falls back to keywords with metrics: null on a lookup failure — the keyword strings themselves still come from the page record, not the failed call. |
queries |
Live GSC top queries for a fixed 28-day window (same shape as the standalone endpoint). | GET /pages/{pageId}/queries (any window) |
gsc_status covers this — not_connected/not_indexed/unavailable as usual. Use the standalone endpoint if you need a custom window. |
outline |
A summary of the latest outline: {id, status, primary_keyword, section_count, completed_at} — not the full sections/SERP snapshot. |
GET /pages/{pageId}/outline (full sections + SERP snapshot) |
null if there's no outline yet, or on a load failure. |
links |
Outbound links extracted from the latest crawl (same shape as the standalone endpoint, same 1000-row cap). | GET /pages/{pageId}/links |
{data: [], meta: {total: 0, truncated: false}} on failure. |
Body size
fields.body.value is the page's full body content rendered as markdown — the same string GET /pages/{pageId}/content returns, which can run large for long-form pages (up to the API's 1,000,000-character save limit). If you only need metadata fields, or you're processing many pages in a batch, be mindful of response size; there's no way to omit fields.body from the brief.
fields.body.content_version_id
The page's current content version id (null if it has none yet). Pass it directly as base_version_id on POST /pages/{pageId}/content — no need to call GET /pages/{pageId}/content first just to get a version to guard against.
When content_excerpt is null
recommendations[].content_excerpt is null in two cases: the recommendation is an add-type rec with no target nodes (nothing existing to excerpt), or a crawl/recrawl happened since the analysis ran and regenerated the document's node ids, so the recommendation's stored target_node_ids no longer resolve. In the second case, POST /pages/{pageId}/analyze to refresh targeting against the current document.
Response 200 (trimmed)
{
"data": {
"page": { "id": 5001, "url": "https://example.com/pricing", "path": "/pricing", "site_id": 42, "title": "Pricing", "primary_keyword": "example pricing", "is_priority": true, "opportunity_score": 68 },
"fields": {
"title": { "value": "Pricing", "status": "snapshot" },
"meta_description": { "value": "See plans and pricing for Example.", "status": "snapshot" },
"h1": { "value": "Pricing", "status": "snapshot" },
"schema": { "value": "{\"@context\":\"https://schema.org\",\"@type\":\"Organization\",\"name\":\"Example\"}", "status": "draft", "validation": { "valid": true, "errors": [], "warnings": ["Missing recommended property \"logo\" for Organization"], "summary": "Schema is valid but has 1 warning to consider." } },
"body": { "value": "# Pricing\n\nChoose a plan that fits your team…", "status": "snapshot", "content_version_id": 287 }
},
"analysis": {
"id": 7200,
"overall_score": 68,
"overall_grade": "C+",
"estimated_score": 76,
"estimated_grade": "B-",
"status": "completed",
"completed_at": "2026-08-10T09:00:00Z",
"score_source": "audit",
"content_stale": false,
"components": [
{ "id": 1, "analysis_id": 7200, "component_type": "title", "label": "Title", "status": "completed", "score": 60, "grade": "C", "estimated_score": 72, "estimated_grade": "B-", "criteria_scores": {}, "rationale": "Keyword appears late in the title.", "model": "claude-haiku", "site_id": 42, "created_at": "2026-08-10T09:00:00Z", "updated_at": "2026-08-10T09:00:00Z" }
]
},
"recommendations": [
{ "id": 5500, "analysis_id": 7200, "page_id": 5001, "site_id": 42, "title": "Lead the title with the primary keyword", "impact": "medium", "status": "pending", "target_field": "title", "content_excerpt": null, "…": "…other Recommendation fields" }
],
"keywords": {
"primary": { "keyword": "example pricing", "metrics": { "keyword": "example pricing", "search_volume": 2400, "keyword_difficulty": 32.5, "cpc": 4.1, "competition": 0.6, "search_intent": "commercial", "monthly_searches": null } },
"secondary": []
},
"queries": {
"data": [{ "query": "example pricing", "clicks": 64, "impressions": 1800, "ctr": 0.0356, "position": 8.2 }],
"gsc_status": "available",
"window": { "start_date": "2026-07-17", "end_date": "2026-08-13" }
},
"outline": { "id": 901, "status": "completed", "primary_keyword": "example pricing", "section_count": 6, "completed_at": "2026-08-10T12:00:42Z" },
"links": { "data": [], "meta": { "total": 0, "truncated": false } }
}
}
fields.*.status mirrors draft status values (snapshot = no draft, serving the crawled/current value; draft/pending/generating/failed otherwise). recommendations[].content_excerpt is populated when resolvable from the current document, same rule as GET /pages/{pageId}/analysis.
fields.schema.validation
fields.schema.validation is the same {valid, errors[], warnings[], summary} shape PUT /pages/{pageId}/drafts/schema returns, computed fresh against whatever fields.schema.value currently resolves to (draft if one exists, else the crawled snapshot's schema). It's null when fields.schema.value is null or an empty string — there's nothing to validate. Note that a page with no schema at all still returns a fields.schema object, not null: {"value": null, "status": "snapshot", "validation": null}. Check fields.schema.value, not fields.schema, to decide whether a page has schema. (fields.schema itself is only null in the failure case, where every field is null.) This is the fastest way to check schema health without a separate call: if value is non-null and validation.valid is false, read validation.errors for what to fix, PUT a corrected draft_value, then resolve any open schema-target recommendation with no body — see Recommendations.
Errors
| Status | When |
|---|---|
401 unauthorized |
Missing/invalid key |
403 forbidden |
Key lacks sites:read |
404 not_found |
No such page in your organization — the only hard failure the brief can produce |
429 rate_limited |
Rate limit exceeded |