Page Identity
The same page shows up in four AI Visibility surfaces, each of which stores the URL it was given:
| Surface | Where the URL comes from |
|---|---|
getAiVisibilityPageScores | the URLs page runs were requested for |
getAiVisibilityBrandCitations | the URLs AI providers cited |
getAiVisibilityDiscoveredPages | the URLs SERP discovery returned |
getAiVisibilityPageRuns | the URL each individual content evaluation ran on |
Those spellings differ. A provider may cite https://example.com/guides/seo-basics?ref=chat, a SERP result may carry https://www.example.com/guides/seo-basics/, and the page run may have been triggered on http://example.com/guides/seo-basics. They are one page.
Every one of these types therefore returns:
| Field | Description | Where |
|---|---|---|
pageId | Stable, opaque id for the page. Identical across all four surfaces. Safe in a URL path segment. | all four |
canonicalUrl | The canonical form of the URL. This is the value pageId is derived from. | all four |
url | A representative raw URL, for linking to the page | all four |
title | The page title as seen in search results, or null (see below) | page scores, cited pages, discovered pages |
AiVisibilityPageRun carries no title: it describes one evaluation run rather than the page, and its pageId resolves to a page-score, cited-page or discovered-page row that does carry one.
Use pageId to join the surfaces and to key client-side routes and caches. Use url only for display and outbound links.
How the canonical URL is derived
In order:
- Surrounding whitespace and the
#fragmentare removed. - An
httporhttpsscheme is replaced withhttps— the two are one page. A URL carrying any other explicit scheme (ftp://,android-app://) is left as it is, so it stays a distinct page; a URL with no scheme is treated ashttps. - A leading
www.is removed, the host is lowercased, and a default:80/:443port is removed. - Trailing slashes are removed from the path. Path case is preserved, because paths are case-sensitive.
- Tracking query parameters are removed. The remaining parameters keep their original order and case.
The parameters removed in step 5 are anything beginning with utm_, plus ref, ref_src, referrer, gclid, gbraid, wbraid, dclid, fbclid, msclkid, yclid, ttclid, twclid, igshid, mc_cid, mc_eid, _ga, _gl, s_kwcid, li_fat_id and epik. Matching is case-insensitive and on whole parameter names, so ?ref=chat is removed while ?refresh=1 is kept.
Parameters that are not in that list are part of the page's identity: https://example.com/p?id=2 and https://example.com/p?id=3 remain two pages.
canonicalUrl uses https for web pages, so for an http-only host it is an identifier rather than a fetchable address. Link to url instead.
Rows are grouped by the canonical URL
getAiVisibilityPageScores, getAiVisibilityBrandCitations and getAiVisibilityDiscoveredPages return one row per page, not one row per URL spelling. A page cited once as …?ref=chat and once as … is a single row with totalBrandCitations: 2, and totalCount counts pages.
url on such a row is a representative spelling: the most recent run's URL for a page score, and the best-positioned discovery's URL for a discovered page. It can differ between two windows of the same query, so compare pageId or canonicalUrl — never url — when matching a row against its previousPeriod entry or against a row from another query.
getAiVisibilityPageRuns still returns one row per run; each run carries the pageId of the page it evaluated.
Filtering by page
getAiVisibilityPageScores accepts either filter:
pageId— apageIdreturned by any AI Visibility query. Takes precedence overurl. A value that is not a page id at all — not base64url, or not encoding ascheme://URL — is rejected with the error codeAI_VISIBILITY_INVALID_PAGE_ID. One that decodes to a URL is canonicalised before matching, like theurlfilter, so it resolves to the page it names.url— any spelling of the page. It is canonicalised before matching, sohttp://www.example.com/p/?utm_source=emailselects the same page ashttps://example.com/p.
query GetAiVisibilityPageScoresByPageId(
$accountId: ObjectID!
$aiVisibilityProjectId: ObjectID!
$aiVisibilityBrandId: ObjectID
$pageId: String
) {
getAiVisibilityPageScores(
accountId: $accountId
aiVisibilityProjectId: $aiVisibilityProjectId
aiVisibilityBrandId: $aiVisibilityBrandId
pageId: $pageId
) {
pageId
canonicalUrl
url
title
totalRuns
latestRunAt
avgQualityScore
totalBrandCitations
}
}
Titles
title is the page title as it appeared in search results, taken from the best-positioned SERP discovery of that page in the project. It is current state, not scoped to the query's dateRange — on every surface that returns it, including discovered pages, where all the other fields are windowed. That is deliberate: a page joined across surfaces on pageId must report one title, and a windowed title would flip between a period and its comparison period for a page the project already has a title for.
It is null for a page SERP discovery has never returned — a URL cited by a provider but absent from search results, for example. Citations and page runs store no title of their own, so there is nothing else to fall back to. Treat null as "unknown" rather than deriving a title from the URL.