{"openapi":"3.1.0","info":{"title":"MonoRanks API","version":"1.0.0","description":"Read a website's scores, issues, actions, pages, search summary, Search Console rows, AI visibility and AI readiness; start rechecks; apply approved SEO field changes, llms.txt and AI crawler rules through the WordPress connector; read and re-run the Lighthouse Agentic browsing check; start full audits and follow them; read every website in one call (portfolio). A key acts as the person who created it. A workspace key (mr_ws_…) covers every website of the workspace or the ones ticked; a website key (mr_site_…) covers one website and is made when the WordPress plugin or the Shopify app connects. Both are listed, edited and revoked in Settings → API and MCP. Signed webhooks for events are described under webhooks. Every value keeps its data state; CTR and position are computed from sums."},"servers":[{"url":"https://app.monoranks.com/api/v1"}],"components":{"securitySchemes":{"siteKey":{"type":"http","scheme":"bearer","description":"Workspace key (mr_ws_…) or website key (mr_site_…), sent as a bearer token. Scopes: sites:read (Read website details and scores), pages:read (Read pages), issues:read (Read issues and actions), search:read (Read search performance), analytics:read (Read Google Analytics conversions and revenue), ai:read (Read AI visibility and AI readiness), content:write (Send content and change alerts), issues:recheck (Start rechecks), audits:run (Start audits), actions:apply (Apply approved fixes through the WordPress connector)."}},"responses":{"Unauthorized":{"description":"Missing, unknown or revoked key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Forbidden":{"description":"Missing scope, other website, or the key owner lost access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Too many requests this hour for this key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}}},"SearchRows":{"type":"object","properties":{"period":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}}},"dimension":{"type":"string"},"searchType":{"type":"string"},"filters":{"type":"object","properties":{"country":{"type":["string","null"]},"device":{"type":["string","null"]},"page":{"type":["string","null"]}}},"window":{"type":["object","null"],"description":"query_page only: the 28 days the pairs cover","properties":{"from":{"type":"string"},"to":{"type":"string"}}},"rows":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string"},"page":{"type":"string"},"date":{"type":"string"},"clicks":{"type":"integer"},"impressions":{"type":"integer"},"ctr":{"type":"number"},"position":{"type":"number"}},"required":["clicks","impressions","ctr","position"]}},"nextCursor":{"type":["string","null"]},"note":{"type":"string"}}}}},"paths":{"/plans":{"get":{"summary":"Public plans and prices (no key): { trialDays, currency, updatedAt, plans }","security":[],"responses":{"200":{"description":"Plans","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/changelog":{"get":{"summary":"Public release notes (no key): the CHANGELOG.md file as Markdown","security":[],"responses":{"200":{"description":"Markdown","content":{"text/markdown":{"schema":{"type":"string"}}}},"404":{"description":"No changelog on this server"}}}},"/sites":{"get":{"summary":"Websites this key can read","security":[{"siteKey":["sites:read"]}],"x-scope":"sites:read","parameters":[],"responses":{"200":{"description":"Websites this key can read","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/portfolio":{"get":{"summary":"Every website the key covers in one call, sorted by name (the Websites list as data): { total, page, per, hasMore, websites: [{ id, name, host, url, platform, status, score: { site, health, aeo, coverage, confidence, version, computedAt, categories }, previousAudit, lastWeek, change: { sincePreviousAudit, sinceLastWeek }, issues: { open, critical, newThisWeek }, actions: { open, waiting }, lastAudit: { id, finishedAt, pagesCrawled, partialReason, coverage }, running, nextScheduledAuditAt, search (search:read only: 28-day clicks and impressions vs the 28 days before, or { connected: false }), link }], searchPeriod }. A website key sees only its website","security":[{"siteKey":["sites:read"]}],"x-scope":"sites:read","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1}},{"name":"per","in":"query","schema":{"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"Every website the key covers in one call, sorted by name (the Websites list as data): { total, page, per, hasMore, websites: [{ id, name, host, url, platform, status, score: { site, health, aeo, coverage, confidence, version, computedAt, categories }, previousAudit, lastWeek, change: { sincePreviousAudit, sinceLastWeek }, issues: { open, critical, newThisWeek }, actions: { open, waiting }, lastAudit: { id, finishedAt, pagesCrawled, partialReason, coverage }, running, nextScheduledAuditAt, search (search:read only: 28-day clicks and impressions vs the 28 days before, or { connected: false }), link }], searchPeriod }. A website key sees only its website","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}":{"get":{"summary":"Website summary: scores (with staleInputs), last audit, open issues, waiting actions, WordPress access, and each connection's freshness (lastImportAt, rowsLastImport, staleDays)","security":[{"siteKey":["sites:read"]}],"x-scope":"sites:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Website summary: scores (with staleInputs), last audit, open issues, waiting actions, WordPress access, and each connection's freshness (lastImportAt, rowsLastImport, staleDays)","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/scores":{"get":{"summary":"Current score and 13 weeks of history with score versions","security":[{"siteKey":["sites:read"]}],"x-scope":"sites:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Current score and 13 weeks of history with score versions","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/performance":{"get":{"summary":"Speed per tested page with real-visitor data next to the lab: { site: { phone, desktop } (Chrome UX Report for the whole site: { available, reason, lcpMs, inpMs, cls, periodEnd, collectedAt } | null), pages: [ { url, template, testedAt, lighthouseVersion, lab: { lcpMs, cls, runs, desktopLcpMs }, field: { scope: page|site, lcpMs, inpMs, cls, periodEnd } | null, findingsUse: { lcp, cls, inp } } ], budget, note, link }. field is real Chrome visitors over 28 days (75th percentile, phone): the page's own record, else the whole site's; null when Google has no data. findingsUse says which number each speed finding uses: field_page, field_site (only when the page is slow in the lab too) or lab","security":[{"siteKey":["sites:read"]}],"x-scope":"sites:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Speed per tested page with real-visitor data next to the lab: { site: { phone, desktop } (Chrome UX Report for the whole site: { available, reason, lcpMs, inpMs, cls, periodEnd, collectedAt } | null), pages: [ { url, template, testedAt, lighthouseVersion, lab: { lcpMs, cls, runs, desktopLcpMs }, field: { scope: page|site, lcpMs, inpMs, cls, periodEnd } | null, findingsUse: { lcp, cls, inp } } ], budget, note, link }. field is real Chrome visitors over 28 days (75th percentile, phone): the page's own record, else the whole site's; null when Google has no data. findingsUse says which number each speed finding uses: field_page, field_site (only when the page is slow in the lab too) or lab","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/issues":{"get":{"summary":"Issues, the same list as the app's Issues screen (default: open, reopened, in progress, recheck queued): { issues: [ { id, ruleId, title, category, severity, confidence, status, affectedPages, template, summary, firstSeen, lastSeen, lastRecheck, link } ] }. lastRecheck is what the last recheck found: { at, present, checked, crawlId, passed, linkingPages?, notRechecked?, note? } or null; notRechecked counts pages the recheck could not read, which keep what the last check saw; note says why a recheck could not judge the issue (it could not run, or had nothing to read). Keyword tasks (KW-*) are left out here and listed under /actions, as in the app","security":[{"siteKey":["issues:read"]}],"x-scope":"issues:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string"},"description":"Comma-separated statuses"}],"responses":{"200":{"description":"Issues, the same list as the app's Issues screen (default: open, reopened, in progress, recheck queued): { issues: [ { id, ruleId, title, category, severity, confidence, status, affectedPages, template, summary, firstSeen, lastSeen, lastRecheck, link } ] }. lastRecheck is what the last recheck found: { at, present, checked, crawlId, passed, linkingPages?, notRechecked?, note? } or null; notRechecked counts pages the recheck could not read, which keep what the last check saw; note says why a recheck could not judge the issue (it could not run, or had nothing to read). Keyword tasks (KW-*) are left out here and listed under /actions, as in the app","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/issues/{issueId}":{"get":{"summary":"Issue with affected pages, evidence and the AI explanation when drafted. For a writable field (writableField other than alt) also suggestions: [{ url, pageId, current, suggested }] per affected page, the same rule-based values the app shows before Apply (suggested is null when there is none), 100 per call with suggestionsTotal, suggestionsOffset, suggestionsLimit, suggestionsNextOffset (pass it as suggestions_offset for the next page; null after the last; suggestionsOffset and suggestionsLimit are accepted as the same parameters) and suggestionsTruncated (more after this page). Each suggestion also has writable (false, with notWritableReason, when apply cannot write that page, for example a category archive that is not a synced WordPress item). Meta description suggestions also carry advice ({ pageKind: legal | archive | empty_archive | account, legalKind, recommended: leave_or_noindex | add_posts_or_noindex | noindex, message } or null; account = a logged-in, cart or checkout page, which should be noindex and gets no description). Too-long titles (SEO-ONP-005) also return titleTemplate ({ suffix, count, note } or null): one shared ending from the title template that makes most titles too long, fixed once in the template. Also lastRecheck (as in the list; a recheck reads every listed page fresh, past page caches, so affected, evidence and suggestions show what the pages say now) and the last approved change batch (lastChange: { batchId, approvedBy, approvedAt, changes: [ { url, field, value, status, appliedAt, live, liveValue, notLiveReason, liveNote, error } ] }; liveNote says when the page shows the value in another letter case, which still counts as live: titles and descriptions are compared ignoring letter case, extra spaces, entities, curly quotes and dash styles), suggestionsNote (titles only, else null: the site changes the letter case of every title, for example Rank Math's Capitalize Titles setting). Change status: approved, applying, applied (written and seen on the live page), not_live (written, but the page still shows another value; notLiveReason says why, including whether the website's page cache was cleared after the write, and the issue stays open), failed, refused (the WordPress plugin cannot write this field on this site, for example under an SEO plugin MonoRanks cannot write into; error says why), reverted","security":[{"siteKey":["issues:read"]}],"x-scope":"issues:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"issueId","in":"path","required":true,"schema":{"type":"string"}},{"name":"suggestions_offset","in":"query","schema":{"type":"integer","minimum":0}},{"name":"suggestions_limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"suggestionsOffset","in":"query","description":"Same as suggestions_offset","schema":{"type":"integer","minimum":0}},{"name":"suggestionsLimit","in":"query","description":"Same as suggestions_limit","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"Issue with affected pages, evidence and the AI explanation when drafted. For a writable field (writableField other than alt) also suggestions: [{ url, pageId, current, suggested }] per affected page, the same rule-based values the app shows before Apply (suggested is null when there is none), 100 per call with suggestionsTotal, suggestionsOffset, suggestionsLimit, suggestionsNextOffset (pass it as suggestions_offset for the next page; null after the last; suggestionsOffset and suggestionsLimit are accepted as the same parameters) and suggestionsTruncated (more after this page). Each suggestion also has writable (false, with notWritableReason, when apply cannot write that page, for example a category archive that is not a synced WordPress item). Meta description suggestions also carry advice ({ pageKind: legal | archive | empty_archive | account, legalKind, recommended: leave_or_noindex | add_posts_or_noindex | noindex, message } or null; account = a logged-in, cart or checkout page, which should be noindex and gets no description). Too-long titles (SEO-ONP-005) also return titleTemplate ({ suffix, count, note } or null): one shared ending from the title template that makes most titles too long, fixed once in the template. Also lastRecheck (as in the list; a recheck reads every listed page fresh, past page caches, so affected, evidence and suggestions show what the pages say now) and the last approved change batch (lastChange: { batchId, approvedBy, approvedAt, changes: [ { url, field, value, status, appliedAt, live, liveValue, notLiveReason, liveNote, error } ] }; liveNote says when the page shows the value in another letter case, which still counts as live: titles and descriptions are compared ignoring letter case, extra spaces, entities, curly quotes and dash styles), suggestionsNote (titles only, else null: the site changes the letter case of every title, for example Rank Math's Capitalize Titles setting). Change status: approved, applying, applied (written and seen on the live page), not_live (written, but the page still shows another value; notLiveReason says why, including whether the website's page cache was cleared after the write, and the issue stays open), failed, refused (the WordPress plugin cannot write this field on this site, for example under an SEO plugin MonoRanks cannot write into; error says why), reverted","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/issues/{issueId}/recheck":{"post":{"summary":"Queue a recheck: { queued, crawlId, note, link }. It reads every page the issue lists again, fresh (past page caches and CDNs); the issue is resolved only when the recheck no longer finds it. A page that does not answer stays listed and is counted in lastRecheck.notRechecked. It also checks again the written changes of the issue's latest batch, even on pages it no longer lists, so a not_live change that has appeared becomes applied. Errors: 409 already_queued; 422 cannot_recheck (the message says why: a keyword task, a resolved or ignored issue, or an issue that lists no pages)","security":[{"siteKey":["issues:recheck"]}],"x-scope":"issues:recheck","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"issueId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Queue a recheck: { queued, crawlId, note, link }. It reads every page the issue lists again, fresh (past page caches and CDNs); the issue is resolved only when the recheck no longer finds it. A page that does not answer stays listed and is counted in lastRecheck.notRechecked. It also checks again the written changes of the issue's latest batch, even on pages it no longer lists, so a not_live change that has appeared becomes applied. Errors: 409 already_queued; 422 cannot_recheck (the message says why: a keyword task, a resolved or ignored issue, or an issue that lists no pages)","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/actions":{"get":{"summary":"Actions ordered by priority with estimated score gain: { waiting, actions: [ { rank, issueId, title, severity, effort, priority, estimatedScoreGain, importance, status, firstSeen, ageDays, waitingWeeks, writableField, link } ] }. `waiting` counts actions open 14 days or more","security":[{"siteKey":["issues:read"]}],"x-scope":"issues:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}}],"responses":{"200":{"description":"Actions ordered by priority with estimated score gain: { waiting, actions: [ { rank, issueId, title, severity, effort, priority, estimatedScoreGain, importance, status, firstSeen, ageDays, waitingWeeks, writableField, link } ] }. `waiting` counts actions open 14 days or more","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/actions/{issueId}/apply":{"post":{"summary":"Approve new values (SEO title, meta description, canonical, noindex) for the pages of one action and write them through the WordPress connector, then recheck: { approved, batchId, note, link }. Same rules as the Apply button: the key owner is recorded as approver and undo is in the app for 30 days. Image alt text and redirects are applied in the app only. Errors: 400 field_not_supported, no_changes, not_a_wordpress_post (the url is not a published WordPress item synced to MonoRanks) or not_applied (with the reason)","security":[{"siteKey":["actions:apply"]}],"x-scope":"actions:apply","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"issueId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Approve new values (SEO title, meta description, canonical, noindex) for the pages of one action and write them through the WordPress connector, then recheck: { approved, batchId, note, link }. Same rules as the Apply button: the key owner is recorded as approver and undo is in the app for 30 days. Image alt text and redirects are applied in the app only. Errors: 400 field_not_supported, no_changes, not_a_wordpress_post (the url is not a published WordPress item synced to MonoRanks) or not_applied (with the reason)","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["changes"],"properties":{"changes":{"type":"array","maxItems":500,"items":{"type":"object","required":["url","value"],"properties":{"url":{"type":"string"},"value":{"type":"string"}}}}}}}}}}},"/sites/{siteId}/pages":{"get":{"summary":"Crawled pages: { total, pages: [ { id, url, status, title, template, pageScore, aeoScore, openIssues, inlinks, lastCrawled } ] }","security":[{"siteKey":["pages:read"]}],"x-scope":"pages:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"q","in":"query","schema":{"type":"string"},"description":"Part of the address or title"},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"per","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Crawled pages: { total, pages: [ { id, url, status, title, template, pageScore, aeoScore, openIssues, inlinks, lastCrawled } ] }","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/pages/{pageId}":{"get":{"summary":"Page facts, scores and findings on the page. Personal email addresses in the page text MonoRanks stores are masked (j•••@gmail.com)","security":[{"siteKey":["pages:read"]}],"x-scope":"pages:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"pageId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Page facts, scores and findings on the page. Personal email addresses in the page text MonoRanks stores are masked (j•••@gmail.com)","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/pages/findings":{"get":{"summary":"Same, looked up by URL","security":[{"siteKey":["pages:read"]}],"x-scope":"pages:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"url","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Same, looked up by URL","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/search/summary":{"get":{"summary":"Search Console totals vs the previous period, top pages and queries","security":[{"siteKey":["search:read"]}],"x-scope":"search:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"days","in":"query","schema":{"type":"integer","enum":[7,28,90]}}],"responses":{"200":{"description":"Search Console totals vs the previous period, top pages and queries","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/search/rows":{"get":{"summary":"Search Console rows for a date range: one row per query, page, query + page pair, or day, as { query?, page?, date?, clicks, impressions, ctr, position }. CTR (a fraction) and position are computed from sums over the whole range, never averaged across rows. Keyset pages in a stable order (clicks high first, then the key; dimension=date by date): send nextCursor back as cursor until it is null. format=csv streams every row from the cursor on as a CSV file (limit is ignored). What is stored limits the filters: country and device work on daily totals only (dimension=date without page); image search has daily totals only; query_page returns the top 5,000 page × query pairs of the 28 days before the last sync inside the range (see window). A filter that the stored data cannot answer returns 400 not_stored","security":[{"siteKey":["search:read"]}],"x-scope":"search:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date"},"description":"First day, YYYY-MM-DD. The range can be at most 16 months, the history MonoRanks keeps."},{"name":"to","in":"query","required":true,"schema":{"type":"string","format":"date"},"description":"Last day, YYYY-MM-DD."},{"name":"dimension","in":"query","schema":{"type":"string","enum":["query","page","query_page","date"],"default":"query"}},{"name":"country","in":"query","schema":{"type":"string","pattern":"^[A-Za-z]{3}$"},"description":"ISO 3166-1 alpha-3 as Search Console uses it, for example usa. dimension=date only."},{"name":"device","in":"query","schema":{"type":"string","enum":["mobile","desktop","tablet"]},"description":"dimension=date only."},{"name":"search_type","in":"query","schema":{"type":"string","enum":["web","image"],"default":"web"}},{"name":"page","in":"query","schema":{"type":"string"},"description":"A full URL matches that page exactly; anything else matches pages whose URL contains it (for example /blog/). Works with dimension=page, query_page and date."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":10000,"default":1000}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"nextCursor from the previous page, unchanged."},{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"],"default":"json"}}],"responses":{"200":{"description":"Rows with nextCursor (JSON), or every row as CSV with format=csv","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRows"}},"text/csv":{"schema":{"type":"string"}}}},"400":{"description":"Bad period, dimension, filter or cursor, or a filter the stored data cannot answer (not_stored)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/topics":{"get":{"summary":"Topics: the website's pages grouped by the folder of their address and the words their Search Console queries share, with search totals per topic: { period, queriesWindow, sort, minPages, searchData, pagesCapped, totalTopics, topics: [ { id, name, nameFrom: queries|section|page, section, words, pageCount, pagesWithoutSearchData, pages: [ { url, clicks, impressions, position } ] (at most 50), pagesTruncated, clicks, impressions, ctr, position, clicksPer28Days, strength: strong|weak|no_clicks, clicksShort, topQueries: [ { query, clicks, impressions, ctr, position } ] (at most 10) } ], ungrouped: { pageCount, clicks, impressions, pages }, uncovered: { pageCount, pages: [ { url, section } ], truncated }, cached, computedAt, note, link }. position is weighted by impressions; strength: strong = at least 5 clicks per page per 28 days, weak = fewer, no_clicks = none; clicksShort = clicks per 28 days short of strong. Groups smaller than minPages go to ungrouped (with search data) or uncovered (crawled content pages without impressions). Pages are matched without scheme, www. and trailing slash. On a website with more than 20,000 Search Console pages the 20,000 with the most impressions are grouped and pagesCapped is true. Deterministic: the same data gives the same ids and names. Kept for an hour per website and period; computed at most 2 times per website per minute. Errors: 400 bad_period (also an end in the future), period_too_long, bad_sort, bad_limit or bad_min_pages; 429 rate_limited","security":[{"siteKey":["search:read"]}],"x-scope":"search:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"start","in":"query","schema":{"type":"string","format":"date"},"description":"First day, YYYY-MM-DD; with end. Default: the last 28 days of final data (ending 3 days ago)."},{"name":"end","in":"query","schema":{"type":"string","format":"date"},"description":"Last day, YYYY-MM-DD, not in the future. Alone: the 28 days ending there."},{"name":"sort","in":"query","schema":{"type":"string","enum":["clicks","opportunity","pages","impressions"],"default":"clicks"},"description":"opportunity: topics with many pages but weak clicks first (largest clicksShort)."},{"name":"minPages","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":2},"description":"Smallest topic, in pages."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Topics: the website's pages grouped by the folder of their address and the words their Search Console queries share, with search totals per topic: { period, queriesWindow, sort, minPages, searchData, pagesCapped, totalTopics, topics: [ { id, name, nameFrom: queries|section|page, section, words, pageCount, pagesWithoutSearchData, pages: [ { url, clicks, impressions, position } ] (at most 50), pagesTruncated, clicks, impressions, ctr, position, clicksPer28Days, strength: strong|weak|no_clicks, clicksShort, topQueries: [ { query, clicks, impressions, ctr, position } ] (at most 10) } ], ungrouped: { pageCount, clicks, impressions, pages }, uncovered: { pageCount, pages: [ { url, section } ], truncated }, cached, computedAt, note, link }. position is weighted by impressions; strength: strong = at least 5 clicks per page per 28 days, weak = fewer, no_clicks = none; clicksShort = clicks per 28 days short of strong. Groups smaller than minPages go to ungrouped (with search data) or uncovered (crawled content pages without impressions). Pages are matched without scheme, www. and trailing slash. On a website with more than 20,000 Search Console pages the 20,000 with the most impressions are grouped and pagesCapped is true. Deterministic: the same data gives the same ids and names. Kept for an hour per website and period; computed at most 2 times per website per minute. Errors: 400 bad_period (also an end in the future), period_too_long, bad_sort, bad_limit or bad_min_pages; 429 rate_limited","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/ai-visibility":{"get":{"summary":"AI visibility per sampling week, newest first: { weeks: [ { week, answers, mentionRate, citationRate, promptsAsked, truncated, prompts: [ { promptId, prompt, locale, active, answers: [ { model, modelName, provider, namedUs, citedUs, citedUrls, sources: [host], answeredAt } ] } ] } ] }. provider is the model maker (openai, anthropic, google). Read from the stored weekly samples (no model is asked on request); the answer text is left out, and so are failed answers","security":[{"siteKey":["ai:read"]}],"x-scope":"ai:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"weeks","in":"query","schema":{"type":"integer","minimum":1,"maximum":12,"default":4},"description":"Sampling weeks back from the current one"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":1000,"default":50},"description":"Questions per week"}],"responses":{"200":{"description":"AI visibility per sampling week, newest first: { weeks: [ { week, answers, mentionRate, citationRate, promptsAsked, truncated, prompts: [ { promptId, prompt, locale, active, answers: [ { model, modelName, provider, namedUs, citedUs, citedUrls, sources: [host], answeredAt } ] } ] } ] }. provider is the model maker (openai, anthropic, google). Read from the stored weekly samples (no model is asked on request); the answer text is left out, and so are failed answers","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/ai-visibility/gaps":{"get":{"summary":"Answer gaps: active questions whose answers in the window never named the website while naming or linking others: { gaps: [ { promptId, prompt, answers, namedUs, citedOthers: [host] } ] }","security":[{"siteKey":["ai:read"]}],"x-scope":"ai:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"weeks","in":"query","schema":{"type":"integer","minimum":1,"maximum":12,"default":4}}],"responses":{"200":{"description":"Answer gaps: active questions whose answers in the window never named the website while naming or linking others: { gaps: [ { promptId, prompt, answers, namedUs, citedOthers: [host] } ] }","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/geo":{"get":{"summary":"AI readiness from the last audit: { checkedAt, robotsTxt, crawlers: [ { bot, owner, purpose, kind, allowed, rule } ], llmsTxt: { present, url, status, bytes, checkedAt } | null, entity: { organizationSchema, organizationName, sameAs, aboutPage, contactPage, napAgrees } | null }. allowed is null when robots.txt could not be read","security":[{"siteKey":["ai:read"]}],"x-scope":"ai:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"AI readiness from the last audit: { checkedAt, robotsTxt, crawlers: [ { bot, owner, purpose, kind, allowed, rule } ], llmsTxt: { present, url, status, bytes, checkedAt } | null, entity: { organizationSchema, organizationName, sameAs, aboutPage, contactPage, napAgrees } | null }. allowed is null when robots.txt could not be read","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/geo/llms-txt":{"get":{"summary":"llms.txt now: { url, live: { found, kind, fetchedKind, message, status, bytes, text, truncated, matchesLastWrite, notVisibleYet, cached: { status, kind, found, cache } | null, fetchedAt }, draft: { text, chars, pages: [ { section, title, url } ] }, wordpress: { connected, writable }, canPublish, lastChange: { batchId, status, value, previous, approvedBy, approvedAt, appliedAt, error } | null, maxChars }. live is /llms.txt fetched on this call, past the website's page cache and CDN (cache-busting query string and no-cache headers); kind is text, html (200 with a web page, not a text file, so found is false), empty, missing, error, unreachable, or stale (MonoRanks wrote it in the last 24 hours but the website still answers with the old version, probably from a cache; notVisibleYet is true and fetchedKind says what it answered), and message says it in words; cached is what the plain address answered when it differs (a cache still holds an older answer); matchesLastWrite is whether it is the value MonoRanks last wrote (null when it never wrote one). draft is generated from the crawled pages, the same as \"Generate from your pages\" in the app; it works without a WordPress connector, for owners who ship the file in code","security":[{"siteKey":["ai:read"]}],"x-scope":"ai:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"llms.txt now: { url, live: { found, kind, fetchedKind, message, status, bytes, text, truncated, matchesLastWrite, notVisibleYet, cached: { status, kind, found, cache } | null, fetchedAt }, draft: { text, chars, pages: [ { section, title, url } ] }, wordpress: { connected, writable }, canPublish, lastChange: { batchId, status, value, previous, approvedBy, approvedAt, appliedAt, error } | null, maxChars }. live is /llms.txt fetched on this call, past the website's page cache and CDN (cache-busting query string and no-cache headers); kind is text, html (200 with a web page, not a text file, so found is false), empty, missing, error, unreachable, or stale (MonoRanks wrote it in the last 24 hours but the website still answers with the old version, probably from a cache; notVisibleYet is true and fetchedKind says what it answered), and message says it in words; cached is what the plain address answered when it differs (a cache still holds an older answer); matchesLastWrite is whether it is the value MonoRanks last wrote (null when it never wrote one). draft is generated from the crawled pages, the same as \"Generate from your pages\" in the app; it works without a WordPress connector, for owners who ship the file in code","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"summary":"Approve a new /llms.txt and write it through the WordPress connector, the same rules as \"Publish /llms.txt\" in the app: WordPress connected and writable, at most 60,000 characters, undo in the app for 30 days, then /llms.txt is read again (GET shows lastChange.status and live.matchesLastWrite). An empty value removes the file MonoRanks wrote. Errors: 409 wordpress_not_connected, wordpress_not_writable or no_change; 400 too_long or bad_value","security":[{"siteKey":["actions:apply"]}],"x-scope":"actions:apply","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Approve a new /llms.txt and write it through the WordPress connector, the same rules as \"Publish /llms.txt\" in the app: WordPress connected and writable, at most 60,000 characters, undo in the app for 30 days, then /llms.txt is read again (GET shows lastChange.status and live.matchesLastWrite). An empty value removes the file MonoRanks wrote. Errors: 409 wordpress_not_connected, wordpress_not_writable or no_change; 400 too_long or bad_value","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["value"],"properties":{"value":{"type":"string","maxLength":60000}}}}}}}},"/sites/{siteId}/geo/ai-crawlers":{"get":{"summary":"AI crawler rules now: { robotsTxt: { url, status, fetchedAt }, crawlers: [ { bot, owner, purpose, kind, allowed, source, rule, matches } ], rules: { bot: allow|deny }, wordpress, canPublish, lastChange }. allowed is what robots.txt, fetched on this call, says to that crawler for the home page (null when it could not be read); rule is what MonoRanks last wrote for it (null: none); matches is whether robots.txt does that rule","security":[{"siteKey":["ai:read"]}],"x-scope":"ai:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"AI crawler rules now: { robotsTxt: { url, status, fetchedAt }, crawlers: [ { bot, owner, purpose, kind, allowed, source, rule, matches } ], rules: { bot: allow|deny }, wordpress, canPublish, lastChange }. allowed is what robots.txt, fetched on this call, says to that crawler for the home page (null when it could not be read); rule is what MonoRanks last wrote for it (null: none); matches is whether robots.txt does that rule","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"summary":"Approve AI crawler rules and write them into robots.txt through the WordPress connector, the same rules as \"Write rules to robots.txt\" in the app (WordPress connected and writable, undo in the app for 30 days, robots.txt read again). Send the full set; crawlers left out get no rule from MonoRanks and {} removes its rules. Errors: 409 wordpress_not_connected, wordpress_not_writable or no_change; 400 bad_rules or unknown_crawler","security":[{"siteKey":["actions:apply"]}],"x-scope":"actions:apply","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Approve AI crawler rules and write them into robots.txt through the WordPress connector, the same rules as \"Write rules to robots.txt\" in the app (WordPress connected and writable, undo in the app for 30 days, robots.txt read again). Send the full set; crawlers left out get no rule from MonoRanks and {} removes its rules. Errors: 409 wordpress_not_connected, wordpress_not_writable or no_change; 400 bad_rules or unknown_crawler","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rules"],"properties":{"rules":{"type":"object","additionalProperties":{"type":"string","enum":["allow","deny"]},"example":{"GPTBot":"deny","OAI-SearchBot":"allow"}}}}}}}}},"/sites/{siteId}/agentic":{"get":{"summary":"Agentic browsing (Lighthouse category: can an AI agent use the page) from the latest stored PageSpeed run per tested URL and device, newest first: { total, page, per, hasMore, results: [ { url, template, strategy, testedAt, lighthouseVersion, passed, applicable, checks: [ { id, title, state: pass|fail|na|info, value, detail?, items? } ] } ], summary: { mobile|desktop: { pages, passed, applicable, checks: [ { id, title, state, pass, fail, detail?, items? } ] } | null }, running: [ { jobId, url, strategy, status, queuedAt } ], budget: { perDay, usedToday, left, nextFreeAt } }. passed of applicable counts pass and fail only. Failing checks add detail (what Lighthouse found, plain text, or null for older runs) and items (up to 5 { selector?, snippet?, url?, message? }) so the reader knows what to fix","security":[{"siteKey":["ai:read"]}],"x-scope":"ai:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"strategy","in":"query","schema":{"type":"string","enum":["mobile","desktop"]},"description":"Only this device; default both"},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"per","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Agentic browsing (Lighthouse category: can an AI agent use the page) from the latest stored PageSpeed run per tested URL and device, newest first: { total, page, per, hasMore, results: [ { url, template, strategy, testedAt, lighthouseVersion, passed, applicable, checks: [ { id, title, state: pass|fail|na|info, value, detail?, items? } ] } ], summary: { mobile|desktop: { pages, passed, applicable, checks: [ { id, title, state, pass, fail, detail?, items? } ] } | null }, running: [ { jobId, url, strategy, status, queuedAt } ], budget: { perDay, usedToday, left, nextFreeAt } }. passed of applicable counts pass and fail only. Failing checks add detail (what Lighthouse found, plain text, or null for older runs) and items (up to 5 { selector?, snippet?, url?, message? }) so the reader knows what to fix","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/agentic/run":{"post":{"summary":"Re-run the Agentic browsing check for one page of this website now (for example after publishing llms.txt): queues one PageSpeed run (performance and Agentic browsing, stored like a speed test) and answers 202 { queued, jobId, status: queued, url, strategy, budget, poll } at once, because a run takes 20 to 60 seconds. Poll GET /sites/{siteId}/agentic/runs/{jobId}. Uses one run of the website's daily PageSpeed budget. Errors: 400 bad_url, other_host (url not on the website's host) or bad_strategy; 409 already_queued (the same url and strategy is waiting); 429 budget_used (the daily PageSpeed budget is used up; nothing is queued) or too_many_runs (3 runs already waiting)","security":[{"siteKey":["issues:recheck"]}],"x-scope":"issues:recheck","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","example":"https://example.com/"},"strategy":{"type":"string","enum":["mobile","desktop"],"default":"mobile"}}}}}}}},"/sites/{siteId}/agentic/runs/{jobId}":{"get":{"summary":"One re-run: { jobId, url, strategy, status: queued|running|done|failed, queuedAt, startedAt, finishedAt, result: { url, template, strategy, testedAt, lighthouseVersion, passed, applicable, checks } | null, error }. error is a plain reason when status is failed (for example the PageSpeed service was out of quota)","security":[{"siteKey":["ai:read"]}],"x-scope":"ai:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"jobId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"One re-run: { jobId, url, strategy, status: queued|running|done|failed, queuedAt, startedAt, finishedAt, result: { url, template, strategy, testedAt, lighthouseVersion, passed, applicable, checks } | null, error }. error is a plain reason when status is failed (for example the PageSpeed service was out of quota)","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/audits":{"get":{"summary":"Full audits: { audits: [Audit] (newest first, default 5, at most 25), active: Audit | null (the crawl queued or running now), nextScheduledAt (the next weekly audit, Sundays 02:00 UTC; null for a paused website), pageBudget: { default, planLimit, websiteSetting }, apiStarts: { perDay: 3, usedToday, left } }. Audit: { id, kind, status: queued|running|done|failed|cancelled, stage, pagesCrawled, pagesDiscovered, pageBudget, timeLimitMinutes, cappedByPlan (true when the website's own budget was above the plan's pages per audit, so the audit got the plan's limit), coverage: { sitemapUrls, sitemapCrawled, percent } | null, partialReason, issuesFound, startedBy: person|schedule, queuedAt, startedAt, finishedAt, error, link }","security":[{"siteKey":["sites:read"]}],"x-scope":"sites:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":25,"default":5}}],"responses":{"200":{"description":"Full audits: { audits: [Audit] (newest first, default 5, at most 25), active: Audit | null (the crawl queued or running now), nextScheduledAt (the next weekly audit, Sundays 02:00 UTC; null for a paused website), pageBudget: { default, planLimit, websiteSetting }, apiStarts: { perDay: 3, usedToday, left } }. Audit: { id, kind, status: queued|running|done|failed|cancelled, stage, pagesCrawled, pagesDiscovered, pageBudget, timeLimitMinutes, cappedByPlan (true when the website's own budget was above the plan's pages per audit, so the audit got the plan's limit), coverage: { sitemapUrls, sitemapCrawled, percent } | null, partialReason, issuesFound, startedBy: person|schedule, queuedAt, startedAt, finishedAt, error, link }","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}},"post":{"summary":"Start a full audit now, the same as the Run audit button (same permission, one crawl per website at a time): answers 202 { queued, auditId, status: queued, pageBudget, timeLimitMinutes, planLimit, startsLeftToday, poll, link } at once. pageBudget is optional: at most the plan's pages per audit (default: the website's audit budget, capped at that limit); a bigger budget than the website's own gets a longer time limit in proportion (at most 120 minutes). Poll GET /sites/{siteId}/audits/{auditId}. Errors: 400 bad_page_budget or over_plan_limit (with planLimit); 409 already_running (with auditId, kind, status and poll of the crawl queued or running); 429 too_many_audits (more than 3 audits per website in 24 hours through the API and MCP; with retryAt)","security":[{"siteKey":["audits:run"]}],"x-scope":"audits:run","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"An audit or recheck is already queued or running","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"pageBudget":{"type":"integer","minimum":1,"example":1000}}}}}}}},"/sites/{siteId}/audits/{auditId}":{"get":{"summary":"One audit (or the recheck an already_running answer named), as an item of GET /audits: status queued|running|done|failed|cancelled, pagesCrawled (updated while it runs), pageBudget, coverage when the sitemap is known, partialReason (page budget reached, page budget of your plan reached, time budget reached, cancelled), startedAt, finishedAt, error","security":[{"siteKey":["sites:read"]}],"x-scope":"sites:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"auditId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"One audit (or the recheck an already_running answer named), as an item of GET /audits: status queued|running|done|failed|cancelled, pagesCrawled (updated while it runs), pageBudget, coverage when the sitemap is known, partialReason (page budget reached, page budget of your plan reached, time budget reached, cancelled), startedAt, finishedAt, error","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/sites/{siteId}/analytics/pages":{"get":{"summary":"Conversions and revenue per landing page from the website's connected GA4 property (read live with the Data API, cached), joined with the stored Search Console clicks, impressions, CTR and position of the same path and days, so content can be ranked by value: { period, channel, property, currency, purchasesMetric, outcomeEvents, totals, sort, order, offset, limit, total, nextOffset, nextCursor, rows: [AnalyticsPage], searchConsole: joined|unavailable, searchConsoleFinalThrough (only when the range ends after the last final Search Console day), cached, fetchedAt, note, link }. 409 ga4_not_connected when GA4 is not connected, 409 ga4_no_access when Google refuses access (reconnect with analytics.readonly), 429 ga4_quota when the property's API quota is used up, 502 ga4_unavailable when Google does not answer.","security":[{"siteKey":["analytics:read"]}],"x-scope":"analytics:read","parameters":[{"name":"siteId","in":"path","required":true,"schema":{"type":"string"}},{"name":"start","in":"query","schema":{"type":"string","format":"date"},"description":"First day, YYYY-MM-DD (from works too). Default: the 28 days ending 3 days ago (the last day of final Search Console data); with end alone, the 28 days ending there. At most 16 months."},{"name":"end","in":"query","schema":{"type":"string","format":"date"},"description":"Last day, YYYY-MM-DD (to works too), not in the future. When it is after the last final Search Console day, the response has searchConsoleFinalThrough."},{"name":"channel","in":"query","schema":{"type":"string","enum":["organic","all"],"default":"organic"},"description":"organic: sessions whose sessionDefaultChannelGroup is Organic Search; all: every channel."},{"name":"sort","in":"query","schema":{"type":"string","enum":["sessions","-sessions","revenue","-revenue","purchase_revenue","-purchase_revenue","purchases","-purchases","key_events","-key_events","conversion_rate","-conversion_rate","revenue_per_session","-revenue_per_session","clicks","-clicks","impressions","-impressions","position","-position"],"default":"sessions"},"description":"A leading - sorts high to low and wins over order (-revenue = sort=revenue&order=desc)."},{"name":"order","in":"query","schema":{"type":"string","enum":["asc","desc"]},"description":"Default desc (asc for position). Rows without a value come last."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":1000,"default":100}},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"nextCursor from the previous page, unchanged (instead of offset)."}],"responses":{"200":{"description":"Pages with nextOffset and nextCursor","content":{"application/json":{"schema":{"type":"object","properties":{"rows":{"type":"array","items":{"type":"object","properties":{"page":{"type":"string","description":"Landing page path as GA4 reports it"},"url":{"type":["string","null"]},"sessions":{"type":"integer"},"engagedSessions":{"type":"integer"},"engagementRate":{"type":"number"},"keyEvents":{"type":"number"},"keyEventsByName":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"count":{"type":"number"}}}},"conversionRate":{"type":"number","description":"Share of sessions with a key event (a fraction)"},"purchases":{"type":["integer","null"]},"purchaseRevenue":{"type":["number","null"]},"revenue":{"type":"number","description":"GA4 totalRevenue in currency"},"revenuePerSession":{"type":"number"},"clicks":{"type":["integer","null"]},"impressions":{"type":["integer","null"]},"ctr":{"type":["number","null"]},"position":{"type":["number","null"]}}}}}}}}},"400":{"description":"Bad parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"GA4 not connected or no access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"description":"Google Analytics did not answer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{"issue.resolved":{"post":{"summary":"A fix was verified by a recheck","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"issue.resolved"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"issue.reopened":{"post":{"summary":"A resolved issue came back","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"issue.reopened"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"audit.finished":{"post":{"summary":"An audit finished (pages, issues, score)","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"audit.finished"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"score.changed":{"post":{"summary":"The site score moved by 2 points or more","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"score.changed"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"connection.broken":{"post":{"summary":"A Google or WordPress connection stopped working","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"connection.broken"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"recheck.failed":{"post":{"summary":"A recheck ran and the issue is still there","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"recheck.failed"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"write.applied":{"post":{"summary":"An approved change was written to the website","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"write.applied"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"write.failed":{"post":{"summary":"An approved change could not be written","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"write.failed"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"write.not_live":{"post":{"summary":"A written change is not on the live page","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"write.not_live"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"agentic.finished":{"post":{"summary":"An Agentic browsing run finished","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"agentic.finished"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}},"ping":{"post":{"summary":"The Test button on an endpoint: { message }","description":"Headers: X-MonoRanks-Event, X-MonoRanks-Delivery (the delivery id) and X-MonoRanks-Signature: sha256=<hex>, the HMAC-SHA256 of the raw body with the endpoint secret. Every body has id, event, at and workspaceId; site events also have siteId, most have host and a link to the app. Answer with any 2xx within 20 seconds; a failed delivery is tried again after 1, 5, 15 and 60 minutes (five tries), and 20 failures in a row switch the endpoint off.","requestBody":{"content":{"application/json":{"schema":{"type":"object","required":["id","event","at","workspaceId"],"properties":{"id":{"type":"string"},"event":{"type":"string","const":"ping"},"at":{"type":"string","format":"date-time"},"workspaceId":{"type":"string"},"siteId":{"type":"string"}}}}}},"responses":{"200":{"description":"Any 2xx counts as delivered"}}}}}}