Sites
Subscription required. Every endpoint on this page but
GET /sitesneeds an active subscription. Without one the request is refused with402 payment_required(Pick a plan to keep going.) before the endpoint runs, reads included.GET /sitesanswers with or without one. See Conventions.
A site is a website you've added to MetaMonster. Sites are the top of the hierarchy — pages, snapshots, analysis, and drafts all belong to a site.
All site endpoints require the sites:read scope.
Site-scoped operations over pages are documented with the pages they act on: GET /sites/{siteId}/pages, PATCH /sites/{siteId}/pages (bulk edit), and POST /sites/{siteId}/analyze (bulk analyze, content:write).
GET /sites
List your organization's sites, newest first. Paginated.
Scope: sites:read
Query parameters
| Parameter | Type | Default | Notes |
|---|---|---|---|
page |
integer | 1 |
1-indexed. Minimum 1. |
limit |
integer | 15 |
Items per page. Minimum 1, maximum 100. |
Request
curl "https://app.metamonster.ai/api/v1/sites?page=1&limit=15" \
-H "Authorization: Bearer mm_YOUR_API_KEY"
Response 200
{
"data": [
{
"id": 42,
"domain": "example.com",
"url": "https://example.com",
"name": "Example",
"created_at": "2026-05-01T12:00:00Z",
"last_crawled_at": "2026-07-20T09:30:00Z",
"page_count": 128,
"draft_count": 5
}
],
"meta": { "total": 1, "page": 1, "per_page": 15 }
}
Each item (site summary shape):
| Field | Type | Description |
|---|---|---|
id |
integer | Site ID |
domain |
string | The site's domain |
url |
string | null | The site's homepage URL, derived from domain: https:// + the lowercased domain, with no trailing slash (e.g. https://example.com). null when domain is not a valid bare hostname — a planned (pre-launch) site whose domain holds a display name, a missing domain, or a domain that includes a path. |
name |
string | null | Display name |
created_at |
string | null | When the site was added (ISO 8601) |
last_crawled_at |
string | null | When the site was last crawled — the latest crawl's completion time, or its start time if still running, else null |
page_count |
integer | Number of pages discovered |
draft_count |
integer | Number of active field drafts across the site |
POST /sites
Add a site to your organization and queue the standard setup job (page discovery → GSC enrichment → crawl) — the API twin of the dashboard's add-site form, minus GSC wiring (API keys carry no user; connect Google Search Console from the dashboard).
Scope: content:write
Request body
| Field | Type | Default | Notes |
|---|---|---|---|
domain |
string | — | Required. With or without a scheme/trailing slash (e.g. example.com or https://example.com/). |
auto_run_keyword_scope |
string | homepage |
One of homepage, top, all. Have the setup job also generate keywords for this scope. Keywords are free and always cover at least the homepage, so none is treated as homepage. |
auto_run_analysis_scope |
string | none |
One of none, homepage, top, all. Have the setup job also run analysis for this scope. |
Both scopes default to none so adding a site over the API never spends keyword or analysis allowance by itself — pass a wider scope only if you want the setup job to run keyword generation and/or analysis right away.
Adding a site requires an active subscription. An org without one gets 402 payment_required and no site is created; there is no free site and no free audit.
Request
curl -X POST https://app.metamonster.ai/api/v1/sites \
-H "Authorization: Bearer mm_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "domain": "example.com" }'
Response 201
{
"data": {
"id": 42,
"domain": "example.com",
"url": "https://example.com",
"name": null,
"business_context": null,
"voice_tone": null,
"gsc_connected": false,
"gsc_property_url": null,
"background_status": "discovering",
"score_curve": null,
"location": { "country_code": null, "country_name": null, "city_code": null, "city_name": null },
"llms_txt": null,
"setup_completed_at": null,
"discovery_completed_at": null,
"created_at": "2026-08-19T00:00:00Z",
"updated_at": "2026-08-19T00:00:00Z",
"last_crawled_at": null,
"latest_crawl": null,
"page_count": 0,
"draft_count": 0
}
}
Same site detail shape as GET /sites/{siteId}. background_status starts at discovering — poll GET /sites/{siteId} until it returns to null before expecting a full page list or opportunity scores.
Errors
| Status | When |
|---|---|
400 invalid_request |
domain is missing or not a valid domain |
402 payment_required |
The org has no active subscription (message: Pick a plan to keep going.) |
GET /sites/{siteId}
Fetch a single site with full detail, including its latest crawl.
Scope: sites:read
Path parameters
| Parameter | Type | Description |
|---|---|---|
siteId |
integer | The site's ID |
Request
curl https://app.metamonster.ai/api/v1/sites/42 \
-H "Authorization: Bearer mm_YOUR_API_KEY"
Response 200
{
"data": {
"id": 42,
"domain": "example.com",
"url": "https://example.com",
"name": "Example",
"business_context": "B2B SaaS for SEO teams",
"voice_tone": "Professional, concise",
"gsc_connected": true,
"gsc_property_url": "sc-domain:example.com",
"background_status": null,
"score_curve": "proportional",
"location": {
"country_code": 2840,
"country_name": "United States",
"city_code": null,
"city_name": null
},
"llms_txt": "…",
"setup_completed_at": "2026-05-01T12:30:00Z",
"discovery_completed_at": "2026-05-01T13:00:00Z",
"created_at": "2026-05-01T12:00:00Z",
"updated_at": "2026-07-20T09:30:00Z",
"last_crawled_at": "2026-07-20T09:30:00Z",
"latest_crawl": {
"id": 900,
"status": "completed",
"total_pages": 128,
"crawled_pages": 128,
"started_at": "2026-07-20T09:00:00Z",
"completed_at": "2026-07-20T09:30:00Z"
},
"page_count": 128,
"draft_count": 5
}
}
Site detail shape:
| Field | Type | Description |
|---|---|---|
id |
integer | Site ID |
domain |
string | The site's domain |
url |
string | null | The site's homepage URL, derived from domain: https:// + the lowercased domain, with no trailing slash (e.g. https://example.com). null when domain is not a valid bare hostname — a planned (pre-launch) site whose domain holds a display name, a missing domain, or a domain that includes a path. |
name |
string | null | Display name |
business_context |
string | null | Business context used to guide content generation |
voice_tone |
string | null | Voice & tone guidance |
gsc_connected |
boolean | null | Whether Google Search Console is connected |
gsc_property_url |
string | null | The connected GSC property |
background_status |
string | null | Non-null while site setup / crawling / keyword generation runs in the background: discovering, enriching, crawling, generating_context. null = idle. Opportunity scores and page lists may be incomplete until it returns to null. |
score_curve |
string | null | Site-level grade-curve setting |
location |
object | Target market — { country_code, country_name, city_code, city_name }, each nullable |
llms_txt |
string | null | Generated llms.txt content |
setup_completed_at |
string | null | When site setup finished |
discovery_completed_at |
string | null | When page discovery finished |
created_at |
string | null | When the site was added |
updated_at |
string | null | Last update |
last_crawled_at |
string | null | Derived from latest_crawl (completion, else start, else null) |
latest_crawl |
object | null | The most recent crawl (see below), or null if never crawled |
page_count |
integer | Number of pages discovered |
draft_count |
integer | Number of active field drafts across the site |
latest_crawl object:
| Field | Type | Description |
|---|---|---|
id |
integer | Crawl ID |
status |
string | Crawl status (e.g. completed, running, failed) |
total_pages |
integer | null | Pages the crawl intended to fetch |
crawled_pages |
integer | null | Pages fetched so far |
started_at |
string | null | When the crawl started |
completed_at |
string | null | When the crawl finished |
Errors
| Status | When |
|---|---|
404 not_found |
No site with that ID in your organization (message: Site not found) |
GET /sites/{siteId}/email-report-settings
Internal preview:
404 not_foundunless email reports are enabled for your organization.
Whether the site's monthly email report is on, and who it goes to. Scope: sites:read.
Request
curl https://app.metamonster.ai/api/v1/sites/42/email-report-settings \
-H "Authorization: Bearer $METAMONSTER_API_KEY"
Response 200
{
"data": {
"enabled": true,
"recipients": ["jade@client.com", "owner@client.com"]
}
}
| Field | Type | Description |
|---|---|---|
enabled |
boolean | The monthly report is on. Off by default. |
recipients |
string[] | Who gets it: lowercased, no duplicates, at most 10. |
Errors
| Status | When |
|---|---|
404 not_found |
No site with that ID in your organization, or email reports aren't enabled for it |
PUT /sites/{siteId}/email-report-settings
Internal preview:
404 not_foundunless email reports are enabled for your organization.
Turn the monthly email report on or off and/or set who it goes to. Scope: content:write.
Request body
Give enabled, recipients or both.
| Field | Type | Description |
|---|---|---|
enabled |
boolean | true sends the monthly report; false stops it. |
recipients |
string[] | The full list of addresses. It replaces the current list. Trimmed, lowercased and de-duplicated; at most 10. |
Turning the report on with no recipients is allowed: nothing is sent until one is added.
Request
curl -X PUT https://app.metamonster.ai/api/v1/sites/42/email-report-settings \
-H "Authorization: Bearer $METAMONSTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "enabled": true, "recipients": ["Jade@Client.com", "owner@client.com"] }'
Response 200
The saved settings, as GET returns them.
Errors
| Status | When |
|---|---|
400 invalid_request |
An invalid address (the message names it, e.g. "bob@" isn't a valid email address.), more than 10 recipients, or neither field given |
404 not_found |
No site with that ID in your organization, or email reports aren't enabled for it |