# Page Identity https://api-docs.lumar.io/docs/ai-visibility/ai-visibility-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: 1. Surrounding whitespace and the `#fragment` are removed. 2. An `http` or `https` scheme is replaced with `https` — 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 as `https`. 3. A leading `www.` is removed, the host is lowercased, and a default `:80` / `:443` port is removed. 4. Trailing slashes are removed from the path. Path case is preserved, because paths are case-sensitive. 5. 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` — a `pageId` returned by any AI Visibility query. Takes precedence over `url`. A value that is not a page id at all — not base64url, or not encoding a `scheme://` URL — is rejected with the error code `AI_VISIBILITY_INVALID_PAGE_ID`. One that decodes to a URL is canonicalised before matching, like the `url` filter, so it resolves to the page it names. - `url` — any spelling of the page. It is canonicalised before matching, so `http://www.example.com/p/?utm_source=email` selects the same page as `https://example.com/p`. ```graphql 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 } } ``` **Variables:** ```json { "accountId": "TjAwN0FjY291bnQxMjM0NQ", "aiVisibilityProjectId": "QWlWaXNpYmlsaXR5UHJvamVjdDE", "aiVisibilityBrandId": "QWlWaXNpYmlsaXR5QnJhbmQx", "pageId": "aHR0cHM6Ly9leGFtcGxlLmNvbS9ndWlkZXMvc2VvLWJhc2ljcw" } ``` ## 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.