URL: /guides/entity-map/cloud

---
title: "The entity map in the cloud"
sidebarTitle: "In the cloud"
icon: "cloud"
description: "The dashboard Entities page, its filters and exports, and the API that serves the same map."
---

A hosted audit produces the same map a local one does. The dashboard draws it, and the API serves it.

## The Entities page

At `/<org>/website/<id>/entities`, reached from the website sidebar or from the Entities button on a report.

It hangs off the **website**, not off one report, because the questions worth asking span audits: did this get better, is the split identity still there, when did that reference break. An audit picker at the top selects which report to draw, defaulting to the newest that produced a map.

### The cards

Six summary numbers, each with its change against the previous audit: entities, types, pages without entities, stable `@id` share, conflicts, and dangling references.

Null and zero are kept apart. A first audit shows no delta at all rather than a change from nothing, and a metric that held still says "No change" rather than showing a zero you have to interpret.

<Note>
The Conflicts card can read **"at least N"**. Three summary counters were added after version 1 shipped, so a map stored before them has them reconstructed from the entities that were stored. A published map is capped, which makes a reconstructed counter a floor rather than a site total. When either side of a comparison was reconstructed the delta is suppressed entirely: one estimate minus one total is not a change, and two estimates are two counts over possibly different caps.
</Note>

### The graph

The same behaviours as the report's viewer, so the hosted graph and an emailed HTML report read the same way: wheel and pinch to zoom, drag to pan, fit to view, node size by occurrences, colour by primary type, hover to highlight an entity's neighbours and dim the rest, click to select, dangling targets drawn hollow.

Labels are a **budget** rather than a zoom threshold: the top twelve by reach when fitted, growing with the square of the zoom, with repeated labels pushed behind every unique one. Without that last rule, forty unnamed `Offer` nodes spend the whole budget drawing the word "Offer" forty times.

Layout runs once and the scene is then static, which is what keeps it usable at two thousand nodes.

<Note>
The graph needs WebGL. Without it the panel says so and the cards, filters and table keep working.
</Note>

### Filters

Free text, page or URL prefix, problem, and the legend, which doubles as the type filter. They apply to the graph and the table together and run **client-side over the whole map**, so clicking a legend chip is instant rather than a round trip.

The filter rules deliberately mirror the API's own, so an export never disagrees with what is on screen.

Page-local entities, a page's own `WebPage`, `BreadcrumbList`, `ListItem`, `Question`, `Answer` and unnamed `ImageObject`, are hidden by default behind a toggle. They usually outnumber everything else: on kinsta.com that is 137 of 188 nodes.

### The table

Sortable, with conflicts and dangling references highlighted. Below `md` the graph is hidden and the table is the primary view. Selecting a row opens the detail panel, docked beside the graph on wider screens and as a sheet below it on narrow ones.

### History

Below the table, and independent of which audit is drawn above it.

**The metric series** plots the six card metrics across every audit of the site that produced a map: entities, types, pages without entities, stable `@id` share, conflicts, and dangling references. The two audits being compared are drawn as larger points. A counter that had to be reconstructed is drawn hollow and labelled "at least", for the same reason the card is.

**The change list** compares two audits. The newer side is the audit the picker at the top of the page selected, so there is one notion of "the audit you are looking at"; the older side is a second picker, and both survive in the URL as `?report=` and `?from=`. Each row opens that entity in the graph, switching audits if it has to.

The rows are grouped, worst first:

| Group | What it means |
|---|---|
| `@id` replaced | The entity kept its identity but moved to a different `@id`. Every page still pointing at the old one now dangles. Usually a plugin swap. |
| Lost a stable `@id` | It is now identified by name and type alone. |
| New conflicts | An entity started carrying disagreeing values for a property across pages. |
| No longer seen | It was in the older audit and is not in the newer one. |
| Gained a stable `@id` | The fix the [identity guide](/guides/entity-map/fixing) describes, landing. |
| Conflicts no longer reported | It was conflicting and is not now. |
| Added | New in the newer audit. |
| Occurrence changes | Declared on more or fewer pages than before. |

<Warning>
**Two of those groups are observations, not conclusions,** and the page labels them "not proven".

The hosted comparison is answered from stored snapshot summaries, which carry a per-entity identity and no page index at all. So it cannot establish that the pages which declared an entity were crawled again. "No longer seen" can mean the markup was deleted, or it can mean the page was simply not visited this time, and from the stored data those are the same reading. A conflict needs two disagreeing declarations to exist, so one of them going uncrawled makes the conflict stop being reported with nothing corrected.

That is why the headings say "No longer seen" rather than "Removed", and "Conflicts no longer reported" rather than "Resolved". Where the newer audit crawled fewer pages than the older one, the page says so and gives both counts, because that is positive evidence for the second reading.

`squirrel entities --diff` does not have this limitation: it reads the uncapped local maps, which keep their page index, so it can separate `removed` from `notCrawled` and prove a fix. See [the entity map on your machine](/guides/entity-map/cli).
</Warning>

<Note>
A row is also flagged individually when the change set argues against it: a conflict that stopped being reported for an entity now declared **fewer** times than before names the alternative explanation on the row.
</Note>

An empty change list says "no tracked change", not "nothing changed". The hosted comparison follows entities appearing, disappearing, changing `@id`, moving occurrence count, and starting or stopping conflicting. It does not follow a conflict count moving between two non-zero values, which property was conflicting, or any property value.

A site with one audit shows the series and an explanation instead of a change list. That is not an error state, and neither is a site whose audits all predate the entity map; a history that fails to load says so and offers a retry.

### If a site has no structured data

The page says so and links to [Fixing structured data identity](/guides/entity-map/fixing) rather than showing an empty graph. A site with no JSON-LD and an audit that stored no map look identical from the outside, and only one of them is worth acting on.

## The API

Three endpoints: two under a report, one under a website. Authentication is the same as the rest of the [API](/api).

### Get the map

```http
GET /v1/reports/{id}/entities
```

| Query | Meaning |
|---|---|
| `format` | `json`, `jsonld`, `html`, `markdown`, `csv`, `dot`, `graphml`, `mermaid`. Default `json`. |
| `type` | Keep only entities carrying this `@type`, matched against every type an entity declares. Case-insensitive. |
| `exclude_type` | Drop entities whose **primary** `@type` is exactly this one. Repeat it to exclude several. Case-sensitive. |
| `page` | Keep only entities declared on a matching page. An absolute `http(s)` URL or a value starting with `/` is a prefix (of the URL, or of the page path and query), anything else is a substring. Case-sensitive. The same rule as `squirrel entities --page` and the MCP tools. |
| `problem` | `no-id`, `conflict`, `dangling`, `single-page`, `split-identity`. |
| `q` | Free-text match over name, `@id`, key and types. |
| `include_page_local` | Include per-page types. Off by default. |

`type` and `exclude_type` read different things on purpose. `type` matches any of an entity's types and ignores case, so `?type=localbusiness` finds an entity typed `["Organization", "LocalBusiness"]`. `exclude_type` matches only the primary type, the one the entity leads with, and matches it exactly. That is the type an entity is listed under in a viewer showing one chip per entity, and comparing it as a string is what keeps a site that declares both `Organization` and `organization` from losing both when you name one. Together that is what makes a deny-list over the visible types expressible:

```http
GET /v1/reports/{id}/entities?exclude_type=WebPage&exclude_type=BreadcrumbList
```

The JSON response is the [map document](/guides/entity-map/format) plus a `meta` object:

```json
{
  "format": "squirrelscan/entity-map",
  "version": 1,
  "site": "https://example.com/",
  "generatedAt": "2026-09-13T05:23:00.000Z",
  "summary": { },
  "nodes": [ ],
  "edges": [ ],
  "pages": [],
  "meta": {
    "reportId": "…",
    "websiteId": "…",
    "auditedAt": "2026-09-13T05:20:00.000Z",
    "filtered": true,
    "counts": { },
    "pageFilterIncomplete": false,
    "backfilled": []
  }
}
```

Three fields in `meta`, and one at the top level, change how you should read the rest.

- **`filtered`** is true when a query filter removed at least one entity. **`summary` still describes the whole audit**; `meta.counts` carries the post-filter totals. Compare the two to see how much of the site you are looking at.
- **`pageFilterIncomplete`** is true when a `page` filter ran against a map holding an entity whose declaring-page list was capped. It says the result **may** be missing entities that really are declared on that page, not that it is. Always false when no `page` filter was given.
- **`backfilled`** names the summary counters that were reconstructed rather than stored, for a map published before they existed. A reconstructed counter can only be computed from the stored entities, and a published map is capped, so it reads low next to its un-reconstructed siblings. Render those as "at least N" rather than as site totals. Entity-level `pageLocal` is reconstructed exactly and is never listed here.
- **`pages`** is empty on hosted maps. The published copy drops the per-page index for size, and the summary carries what a reader needs from it. Empty means not published, not "no pages".

<Warning>
This is the opposite of the local behaviour, and it is the difference most likely to catch you out. `squirrel entities --type Organization` returns a summary describing **the organizations that survived the filter**. The API returns the same filtered nodes with a summary describing **the whole site**, and puts the filtered totals in `meta.counts`.

Neither is wrong, but a script that reads `summary.nodeCount` after filtering gets a different number from each.
</Warning>

<Warning>
The `page` filter here is a **prefix** match, so `https://site/about` also matches `https://site/about-us`; add a trailing slash to scope to a directory. The CLI and the MCP tools use a substring match instead. Comparing hosted output against local output on the same filter will show the difference.
</Warning>

### A hosted map is a projection

The local document is uncapped, because a file on disk costs nothing. The published copy is measured against a hard size gate, so it keeps the entities and references that carry the findings and drops the tail:

| Limit | Published |
|---|---|
| Entities | 750 |
| References | 1,500 |
| Declaring pages per entity | 5 |
| Distinct values per conflicting property | 5 |
| Any single string property | 512 characters |
| The whole map | 512 KB |

The page cap is the one to watch. Locally an entity lists up to 50 of its declaring pages; hosted it lists 5, with the rest in `morePages`. Anything that needs the real set has to count `pages.length + morePages` rather than reading the array, and a `page` filter over a hosted map is correspondingly more likely to set `pageFilterIncomplete`.

Which 750 entities survive is a sample, not the head of the list. A slice of the budget, `maxProblemShare` at 0.2 of the node budget and a ceiling rather than a target, is reserved first for entities that carry a finding the entity rules report, so a finding on the report always has an entity in the hosted graph to open. The classes are:

- an entity with conflicting properties, including a type disagreement
- an entity with references to an `@id` nothing declares
- a multi-page `Organization`, `LocalBusiness`, `Person` or `WebSite` with no `@id`
- the same entity declared under two `@id`s on one page
- an identified entity nothing references, declared on one page
- an `@id` that is not an absolute URL
- the primary organization when it has no `sameAs`
- a publisher that disagrees with a clear majority of the site (a balanced split between several publishers is not a finding)
- a `Person` author with no `url` or `sameAs`
- a `LocalBusiness` declared on nearly every crawled page

Each class present keeps at least one entity while the budget allows, even where that goes over the ceiling on a very small budget, taking turns so one class cannot crowd out another. The reserved entities still count against the 750 and the 512 KB, and `truncated` still reports every entity and reference dropped. Findings about something absent, such as a missing `Organization` or `WebSite`, have no entity to keep.

Entity and reference counts alone would not hold the size, because property values are site-controlled strings: fifty conflict values of a megabyte each would pass a node count and still blow the limit. The byte ceiling is the real backstop and the per-string caps are what usually keep it from engaging.

### Get one entity

```http
GET /v1/reports/{id}/entities/{key}
```

One node with its properties, occurrences, conflicts, declaring pages, and the edges in and out of it.

`key` accepts the node key, the entity's `@id`, or its exact display name. An `@id` is a URL, so percent-encode it and escape `/` as `%2F`:

```bash
curl -H "Authorization: Bearer $SQUIRRELSCAN_API_KEY" \
  "https://api.squirrelscan.com/v1/reports/$REPORT/entities/https%3A%2F%2Fexample.com%2F%23organization"
```

### Get the history

```http
GET /v1/websites/{id}/entities/history
```

One summary point per audit that produced a map, plus the change set between two of them. It is what the dashboard's history view draws.

| Query | Meaning |
|---|---|
| `from` | Report id of the older side. Defaults to the audit immediately before `to`. |
| `to` | Report id of the newer side. Defaults to the latest audit with a map. |
| `limit` | Points in the `series`, newest first. Default 50, maximum 200. |

```json
{
  "websiteId": "…",
  "series": [
    {
      "reportId": "…",
      "runId": "…",
      "auditedAt": "2026-09-13T05:20:00.000Z",
      "summary": { },
      "backfilled": []
    }
  ],
  "comparison": {
    "from": { "reportId": "…", "auditedAt": "2026-08-13T05:20:00.000Z" },
    "to": { "reportId": "…", "auditedAt": "2026-09-13T05:20:00.000Z" }
  },
  "changes": {
    "added": [], "removed": [], "gainedId": [], "lostId": [], "changedId": [],
    "occurrenceDeltas": [], "newConflicts": [], "resolvedConflicts": [],
    "summaryDelta": []
  }
}
```

Things worth knowing before you build on it:

- **`limit` bounds the `series` only.** `from` and `to` resolve by their own lookup, so they can name an audit older than the window. A report id that is not one of this website's snapshots is a 404, never a silent fall back to the default pair.
- **`comparison` and `changes` are null together** when the site has fewer than two snapshots, or when `from` and `to` name the same one. That is a first audit, not an error.
- **`from` must be at or before `to`.** An inverted pair is a 400 rather than a quiet swap, because the response labels the two sides and reversing your arguments would answer a different question.
- **`backfilled` is per point**, not per series. Rows were written at different times, so an older snapshot can be missing a counter a newer one recorded, and one flag for the whole series would either over-claim or under-claim.
- **`summaryDelta` only names metrics that moved**, and its metric list is not the same as the dashboard's six cards: it has no `conflictCount` and no notion of a type count. Derive those from two points in `series` rather than expecting them here.
- **`newConflicts` and `resolvedConflicts` are per entity, as counts**, carrying `before` and `after`. They cannot name the property, and they only fire on a transition through zero. A conflict count moving from 2 to 1 produces no row.
- **`removed` and `resolvedConflicts` are not proven.** Same limitation as the dashboard above: the snapshot carries no page index, so absence cannot be told from a page that was not crawled again. Treat both as observations.
- **Dangling movement and not-crawled reconciliation are not here yet.** `squirrel entities --diff` reports them from the local maps.

### Untrusted content

Every entity name, `@id`, type and property value in the map is declared by the audited site. The structure around them is not: the discriminators, timestamps, key prefixes, counts and any generated `@id` are squirrelscan's.

The `html` rendering escapes it and is safe to open. The `markdown` rendering escapes only table delimiters, which is enough while it is read as markdown: **if you render it to HTML, sanitize it first**, or take `format=json` and render the fields yourself.

## Exports

The dashboard's export menu offers the same formats as the API. Anything the hosted renderer does not implement is shown disabled rather than hidden, so the menu and the documented contract agree rather than quietly diverging.

## References

- [The entity map](/guides/entity-map): what it is and how to read it
- [The entity map format](/guides/entity-map/format): the documents these endpoints return
- [The entity map on your machine](/guides/entity-map/cli): the same map locally
- [The entity map for agents](/guides/entity-map/mcp): the MCP tools over the same data
- [Fixing structured data identity](/guides/entity-map/fixing): the change to make in each generator
- [API reference](/api): authentication and the rest of the surface
