> For the complete documentation index, see [llms.txt](https://docs.multiset.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.multiset.ai/fundamentals/rest-api-docs/pano-360-virtual-tour.md).

# 360° Virtual Tour (Pano)

Read APIs that serve a navigable 360 panoramic tour as a graph of nodes, including manifests, pagination and tours across map versions and MapSets.

The Pano APIs serve navigable **360° panoramic virtual tours** built from your maps. A tour is a graph of **nodes**. Each node is a 360° capture point with a position, orientation, a panorama image, and links to its neighbours. Use these read APIs to build a "walk-through" viewer where a user steps from node to node.

All Pano endpoints are read-only and require your authentication token. They are mounted under **`/v1/pano`**.

{% hint style="info" %}
Pano data exists only for maps that were **created with panoramas requested**. Set `source.metadata.assets` to `["pano"]` at upload, on a **360 video** capture or an **`.e57` or MatterPak** scan, see [Optional assets](/fundamentals/rest-api-docs/map-upload.md#optional-assets). Every other map returns `404` from these endpoints, including a panoramic scan that was uploaded without the opt-in, so a `404` means "no tour on this map" rather than "something went wrong". See [360° Virtual Tour](/fundamentals/maps/output-asset/360-virtual-tour.md) for the concepts behind these calls and how to preview a tour in the Developer Portal.
{% endhint %}

### The node object

Most responses return one or more **node** objects with this shape:

```json
{
    "nodeId": "000012",
    "keyframe": 12,
    "timeS": 4.5,
    "position": [1.20, 1.60, -3.40],
    "rotation": [0.0, 0.0, 0.0, 1.0],
    "rgbKey": "…/Pano/RGB/1024/000012.jpg",
    "rgbKeys": {
        "512":  "…/Pano/RGB/512/000012.jpg",
        "1024": "…/Pano/RGB/1024/000012.jpg",
        "4096": "…/Pano/RGB/4096/000012.jpg"
    },
    "maskKey": null,
    "neighbors": [ { "nodeId": "000013", "distance": 1.05 } ]
}
```

* **`position`**: node location `[x, y, z]` in the map's coordinate frame.
* **`rotation`**: orientation quaternion `[qx, qy, qz, qw]`.
* **`keyframe`** and **`timeS`**: the source video frame index and its timestamp in seconds. Present on tours built from **360 video**. Absent on tours built from **`.e57` or MatterPak** scans, whose nodes come from scan stations rather than video frames, so treat both as optional.
* **`rgbKeys`**: the node's 360° panorama at each available resolution, keyed by equirect width. **`rgbKey`** is a single default tier for clients that don't select one. See [Panorama images](#panorama-images) below.
* **`maskKey`**: person-mask image key for this node, or `null`. Populated only when the map was created with `panoMask` in `source.metadata.assets`, see [Optional assets](/fundamentals/rest-api-docs/map-upload.md#optional-assets). Use it to hide people captured while scanning.
* **`neighbors`**: the nodes you can step to from here, with the distance in metres.

### Panorama images

Image fields are **storage keys, not URLs**. Exchange a key for a temporary download URL:

```
GET /v1/file?key=…/Pano/RGB/1024/000012.jpg
→ { "url": "https://…" }
```

Each panorama is published at several resolutions, listed in the tour header's `rgbSizes` (typically `[512, 1024, 4096]`). `rgbKeys` holds one key per **equirect width**, so a viewer can show a small image immediately and sharpen it in place. `rgbKey` is a single default tier (1024) if you'd rather not choose.

```js
// Small image for neighbours the user may step to next.
const preview = node.rgbKeys["512"];

// Sharpest image for the node the user is standing in.
const widths = Object.keys(node.rgbKeys).map(Number);
const full = node.rgbKeys[String(Math.max(...widths))];
```

Read the largest width out of `rgbKeys` rather than hard-coding `"4096"`, because the available widths can differ between tours.

### Orienting a panorama

The images are equirectangular and **not** heading-stabilised, so a viewer must apply each node's `rotation` to place it in the world. These conventions are the same for every tour:

* **World frame**: right-handed, with `+X` right, `+Y` up, and `+Z` back.
* **Node frame**: OpenGL/ARKit convention; `rotation` maps node-local → world.
* **Equirect mapping**: the centre column of the image (`u = 0.5`) looks along the node's local `−Z`. Horizontally, `yaw(u) = (u − 0.5) × 360°` about `+Y`, positive toward `+X`. Vertically, `v = 0` is the zenith (`+90°`) and `v = 1` the nadir (`−90°`).

### Endpoints

#### List pano-enabled maps

**`GET /v1/pano`**: a paginated library of maps that have a virtual tour.

Query params: `page` (default `1`), `limit` (1–100, default `20`).

```json
{
    "totalCount": 8,
    "page": 1,
    "limit": 20,
    "panos": [
        {
            "mapCode": "MAP_ABC123DEF456",
            "mapId": "67e1…",
            "mapName": "Ground Floor",
            "thumbnailKey": "…/RGB/Rgb_4db3.jpg",
            "metric": true,
            "panoSize": "4096x2048",
            "rgbSizes": [512, 1024, 4096],
            "minSpacingM": 1.0,
            "nodeCount": 240,
            "updatedAt": "2026-07-20T10:15:00.000Z"
        }
    ]
}
```

{% openapi src="/files/T8MNstj6ZwhVHDx9RLt6" path="/pano" method="get" %}
[pano-api.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-2d117c4bb34ae1e459330f1d503cbed24d9621bc%2Fpano-api.yaml?alt=media)
{% endopenapi %}

#### Tour summary

**`GET /v1/pano/{id}`**: header/summary for one map's tour (`{id}` = map id or map code). Includes `nodeCount`, `hasMasks`, the resolutions the tour ships (`rgbSizes`, `panoSize`), and navigation hints (`version`, `mapSet`) when the map belongs to a version chain or MapSet.

{% openapi src="/files/T8MNstj6ZwhVHDx9RLt6" path="/pano/{id}" method="get" %}
[pano-api.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-2d117c4bb34ae1e459330f1d503cbed24d9621bc%2Fpano-api.yaml?alt=media)
{% endopenapi %}

#### Viewer-ready manifest

**`GET /v1/pano/{id}/manifest`**: the tour header plus **every node**, sorted by `nodeId`. Load this once to render a complete tour.

{% openapi src="/files/T8MNstj6ZwhVHDx9RLt6" path="/pano/{id}/manifest" method="get" %}
[pano-api.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-2d117c4bb34ae1e459330f1d503cbed24d9621bc%2Fpano-api.yaml?alt=media)
{% endopenapi %}

#### Paginated nodes

**`GET /v1/pano/{id}/nodes`**: nodes in pages, for large tours. Query params: `page` (default `1`), `limit` (1–200, default `50`). Returns `{ totalCount, page, limit, nodes }`.

{% openapi src="/files/T8MNstj6ZwhVHDx9RLt6" path="/pano/{id}/nodes" method="get" %}
[pano-api.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-2d117c4bb34ae1e459330f1d503cbed24d9621bc%2Fpano-api.yaml?alt=media)
{% endopenapi %}

#### Navigation window

**`GET /v1/pano/{id}/window`**: the local neighbourhood around a starting point, for stepping through a tour without loading it all. All query params are optional:

* **`nodeId`**: centre on a specific node.
* **`x`, `y`, `z`**: centre on the node nearest a world position (provide all three).
* **`qx`, `qy`, `qz`, `qw`**: an optional orientation quaternion (provide all four, together with a position) to prefer nodes the user is facing.
* **`depth`**: how many graph hops to include from the centre (1–4, default `2`).

You cannot pass both `nodeId` and a position. Precedence is `nodeId` → position → the tour's entry node.

```json
{
    "mapCode": "MAP_ABC123DEF456",
    "mapId": "67e1…",
    "center": "000012",
    "centerDistanceM": 0.42,   /* only when centring by position */
    "centerAngleDeg": 12.5,    /* only when a quaternion was also sent */
    "depth": 2,
    "nodeCount": 9,
    "nodes": [ /* node objects within `depth` hops of the centre */ ]
}
```

`centerDistanceM` tells you how far the chosen node sits from the position you asked for, and `centerAngleDeg` how far its heading differs from the orientation you passed. Both appear only when you centre by position (the second only when you also send a quaternion).

{% openapi src="/files/T8MNstj6ZwhVHDx9RLt6" path="/pano/{id}/window" method="get" %}
[pano-api.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-2d117c4bb34ae1e459330f1d503cbed24d9621bc%2Fpano-api.yaml?alt=media)
{% endopenapi %}

### Tours across versions and MapSets

#### Same view across map versions

**`GET /v1/pano/map-version/{versionCode}/window`**: returns the "same viewpoint" seen across the maps in a version chain, so a viewer can switch between versions (e.g. *before* / *after* an update) at the same spot. Optional query params: `mapCodes` (comma-separated subset of the version), `anchorMapCode` (which version drives navigation), `nodeId`, `depth` (1–4, default `2`), `maxDistanceM` (how close a node must be to count as the "same view"). Positions are returned in the base map's frame.

{% openapi src="/files/T8MNstj6ZwhVHDx9RLt6" path="/pano/map-version/{versionCode}/window" method="get" %}
[pano-api.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-2d117c4bb34ae1e459330f1d503cbed24d9621bc%2Fpano-api.yaml?alt=media)
{% endopenapi %}

#### One continuous tour across a MapSet

**`GET /v1/pano/map-set/{id}/window`**: treats the pano-enabled maps in a MapSet as a **single continuous tour**, bridging between maps where they meet. Optional query params: `mapCodes`, `mapCode` (the current node's map), `nodeId`, `depth` (1–4, default `2`), `bridgeRadiusM` (how close nodes in different maps must be to link). Each returned node is identified by `{ mapCode, nodeId }`, and neighbours that cross into another map are marked `"cross": true`.

{% openapi src="/files/T8MNstj6ZwhVHDx9RLt6" path="/pano/map-set/{id}/window" method="get" %}
[pano-api.yaml](https://3163433004-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FokTDI7QVY04Zvb1pQ8Ry%2Fuploads%2Fgit-blob-2d117c4bb34ae1e459330f1d503cbed24d9621bc%2Fpano-api.yaml?alt=media)
{% endopenapi %}

#### The whole MapSet tour in one call

**`GET /v1/pano/map-set/{id}/manifest`**: returns **every** panorama node across the pano-enabled maps in a MapSet, already transformed into the set's common frame, with neighbours bridged across map boundaries exactly as the window endpoint bridges them. Previously a client had to walk each map separately and stitch the result together.

Optional query params: `mapCodes` (comma-separated subset of the set), and `bridgeRadiusM` (how close nodes in different maps must be to link). When `bridgeRadiusM` is omitted it defaults to the largest node spacing among the included maps, which is usually what you want.

Each node is identified by `{ mapCode, nodeId }`, the same shape the window endpoints return.

{% hint style="info" %}
For large tours, prefer the **window** endpoints and load neighbours as the user moves, rather than fetching the full manifest up front.

Reach for the manifest when you need the whole graph at once: drawing a complete floor plan or minimap, precomputing a route, or exporting a tour. On a big multi-floor set it is a large response.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.multiset.ai/fundamentals/rest-api-docs/pano-360-virtual-tour.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
