For the complete documentation index, see llms.txt. This page is also available as Markdown.

360° Virtual Tour (Pano)

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.

Pano data is generated for maps built from panoramic input: 360 video captures (Insta360) and .e57 or MatterPak scans with embedded panoramas. Maps without pano data return 404. See 360° Virtual Tour for the concepts behind these calls and how to preview a tour in the Developer Portal.

The node object

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

{
    "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].

  • 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 below.

  • maskKey: optional person-mask image key (null if none).

  • 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:

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.

Read the largest width out of rgbKeys rather than hard-coding "4096" — 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 — +X right, +Y up, +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).

List pano-enabled maps

get

A paginated library of maps that have a virtual tour, newest first.

Authorizations
AuthorizationstringRequired

JWT token obtained from the authentication endpoint.

Query parameters
pageinteger · min: 1OptionalDefault: 1
limitinteger · min: 1 · max: 100OptionalDefault: 20
Responses
200

Paginated list of pano-enabled maps.

application/json
totalCountintegerOptionalExample: 8
pageintegerOptionalExample: 1
limitintegerOptionalExample: 20
get/pano
GET /v1/pano HTTP/1.1
Host: api.multiset.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "totalCount": 8,
  "page": 1,
  "limit": 20,
  "panos": [
    {
      "mapCode": "MAP_ABC123DEF456",
      "mapId": "text",
      "mapName": "Ground Floor",
      "thumbnailKey": "text",
      "metric": true,
      "panoSize": "4096x2048",
      "rgbSizes": [
        512,
        1024,
        4096
      ],
      "minSpacingM": 1,
      "nodeCount": 240,
      "updatedAt": "2026-01-01T00:00:00.000Z"
    }
  ]
}

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.

Get tour summary

get

Header/summary for one map's tour, including navigation hints when the map belongs to a version chain or MapSet.

Authorizations
AuthorizationstringRequired

JWT token obtained from the authentication endpoint.

Path parameters
idstringRequired

Map id or map code.

Example: MAP_ABC123DEF456
Responses
200

Tour header.

application/json
mapCodestringOptionalExample: MAP_ABC123DEF456
mapIdstringOptional
mapNamestringOptionalExample: Ground Floor
thumbnailKeystring · nullableOptional

Map thumbnail storage key.

metricbooleanOptional
panoSizestringOptional

Full-res equirect dimensions — the largest tier.

Example: 4096x2048
rgbSizesinteger[]Optional

Equirect widths available for this tour, ascending.

Example: [512,1024,4096]
minSpacingMnumberOptionalExample: 1
nodeCountintegerOptionalExample: 240
hasMasksbooleanOptional
createdAtstring · date-timeOptional
updatedAtstring · date-timeOptional
get/pano/{id}
GET /v1/pano/{id} HTTP/1.1
Host: api.multiset.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "mapCode": "MAP_ABC123DEF456",
  "mapId": "text",
  "mapName": "Ground Floor",
  "thumbnailKey": "text",
  "metric": true,
  "panoSize": "4096x2048",
  "rgbSizes": [
    512,
    1024,
    4096
  ],
  "minSpacingM": 1,
  "nodeCount": 240,
  "hasMasks": true,
  "version": {
    "versionCode": "text",
    "versionCount": 1
  },
  "mapSet": {
    "mapSetCode": "text"
  },
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z"
}

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.

Get viewer-ready manifest

get

The tour header plus every node, sorted by nodeId. Load once to render a complete tour.

Authorizations
AuthorizationstringRequired

JWT token obtained from the authentication endpoint.

Path parameters
idstringRequired

Map id or map code.

Example: MAP_ABC123DEF456
Responses
200

Header plus all nodes.

application/json
mapCodestringOptionalExample: MAP_ABC123DEF456
mapIdstringOptional
mapNamestringOptionalExample: Ground Floor
thumbnailKeystring · nullableOptional

Map thumbnail storage key.

metricbooleanOptional
panoSizestringOptional

Full-res equirect dimensions — the largest tier.

Example: 4096x2048
rgbSizesinteger[]Optional

Equirect widths available for this tour, ascending.

Example: [512,1024,4096]
minSpacingMnumberOptionalExample: 1
nodeCountintegerOptionalExample: 240
hasMasksbooleanOptional
createdAtstring · date-timeOptional
updatedAtstring · date-timeOptional
get/pano/{id}/manifest
GET /v1/pano/{id}/manifest HTTP/1.1
Host: api.multiset.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "mapCode": "MAP_ABC123DEF456",
  "mapId": "text",
  "mapName": "Ground Floor",
  "thumbnailKey": "text",
  "metric": true,
  "panoSize": "4096x2048",
  "rgbSizes": [
    512,
    1024,
    4096
  ],
  "minSpacingM": 1,
  "nodeCount": 240,
  "hasMasks": true,
  "version": {
    "versionCode": "text",
    "versionCount": 1
  },
  "mapSet": {
    "mapSetCode": "text"
  },
  "createdAt": "2026-01-01T00:00:00.000Z",
  "updatedAt": "2026-01-01T00:00:00.000Z",
  "nodes": [
    {
      "nodeId": "000012",
      "keyframe": 12,
      "timeS": 4.5,
      "position": [
        1.2,
        1.6,
        -3.4
      ],
      "rotation": [
        0,
        0,
        0,
        1
      ],
      "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": "text",
      "neighbors": [
        {
          "nodeId": "000013",
          "distance": 1.05
        }
      ]
    }
  ]
}

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 }.

Get nodes (paginated)

get

Tour nodes in pages, for large tours.

Authorizations
AuthorizationstringRequired

JWT token obtained from the authentication endpoint.

Path parameters
idstringRequired

Map id or map code.

Example: MAP_ABC123DEF456
Query parameters
pageinteger · min: 1OptionalDefault: 1
limitinteger · min: 1 · max: 200OptionalDefault: 50
Responses
200

Paginated nodes.

application/json
totalCountintegerOptional
pageintegerOptional
limitintegerOptional
get/pano/{id}/nodes
GET /v1/pano/{id}/nodes HTTP/1.1
Host: api.multiset.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "totalCount": 1,
  "page": 1,
  "limit": 1,
  "nodes": [
    {
      "nodeId": "000012",
      "keyframe": 12,
      "timeS": 4.5,
      "position": [
        1.2,
        1.6,
        -3.4
      ],
      "rotation": [
        0,
        0,
        0,
        1
      ],
      "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": "text",
      "neighbors": [
        {
          "nodeId": "000013",
          "distance": 1.05
        }
      ]
    }
  ]
}

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.

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).

Get navigation window

get

The local neighbourhood around a starting point, for stepping through a tour without loading it all. Center on a nodeId, or on the node nearest a world position (x,y,z), optionally biased by an orientation quaternion (qx,qy,qz,qw). You cannot pass both nodeId and a position. Precedence: nodeId → position → tour entry node.

Authorizations
AuthorizationstringRequired

JWT token obtained from the authentication endpoint.

Path parameters
idstringRequired

Map id or map code.

Example: MAP_ABC123DEF456
Query parameters
nodeIdstringOptional

Center on this node.

depthinteger · min: 1 · max: 4Optional

Graph hops from the center.

Default: 2
xnumberOptional

World X (provide x

ynumberOptional
znumberOptional
qxnumberOptional

Orientation quaternion (provide all four

qynumberOptional
qznumberOptional
qwnumberOptional
Responses
200

Window of nodes around the center.

application/json
mapCodestringOptional
mapIdstringOptional
centerstring · nullableOptional
centerDistanceMnumberOptional

Present when centered by position.

centerAngleDegnumberOptional

Present when centered by position + orientation.

depthintegerOptional
nodeCountintegerOptional
get/pano/{id}/window
GET /v1/pano/{id}/window HTTP/1.1
Host: api.multiset.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "mapCode": "text",
  "mapId": "text",
  "center": "text",
  "centerDistanceM": 1,
  "centerAngleDeg": 1,
  "depth": 1,
  "nodeCount": 1,
  "nodes": [
    {
      "nodeId": "000012",
      "keyframe": 12,
      "timeS": 4.5,
      "position": [
        1.2,
        1.6,
        -3.4
      ],
      "rotation": [
        0,
        0,
        0,
        1
      ],
      "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": "text",
      "neighbors": [
        {
          "nodeId": "000013",
          "distance": 1.05
        }
      ]
    }
  ]
}

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.

Same view across map versions

get

Returns the "same viewpoint" across the maps in a version chain, so a viewer can switch between versions at the same spot. Positions are returned in the base map's frame.

Authorizations
AuthorizationstringRequired

JWT token obtained from the authentication endpoint.

Path parameters
versionCodestringRequired
Query parameters
mapCodesstringOptional

Comma-separated version mapCodes (default all in chain).

anchorMapCodestringOptional

Which version drives navigation (default base map).

nodeIdstringOptional
depthinteger · min: 1 · max: 4OptionalDefault: 2
maxDistanceMnumberOptional

Same-view gate (default no cap).

Responses
200

Cross-version viewpoints.

application/json
versionCodestringOptional
baseMapCodestringOptional
maxDistanceMnumber · nullableOptional
windowMapCodesstring[]Optional
viewpointCountintegerOptional
get/pano/map-version/{versionCode}/window
GET /v1/pano/map-version/{versionCode}/window HTTP/1.1
Host: api.multiset.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "versionCode": "text",
  "baseMapCode": "text",
  "maxDistanceM": 1,
  "anchor": {
    "mapCode": "text",
    "center": "text",
    "depth": 1
  },
  "windowMapCodes": [
    "text"
  ],
  "viewpointCount": 1,
  "viewpoints": [
    {
      "anchorNodeId": "text",
      "isCenter": true,
      "position": [
        1
      ],
      "neighbors": [
        "text"
      ],
      "byVersion": {
        "ANY_ADDITIONAL_PROPERTY": {
          "nodeId": "000046",
          "keyframe": 46,
          "timeS": 45,
          "position": [
            1
          ],
          "rotation": [
            1
          ],
          "rgbKey": "…/Pano/RGB/1024/000046.jpg",
          "rgbKeys": {
            "ANY_ADDITIONAL_PROPERTY": "text"
          },
          "maskKey": "text",
          "distanceM": 0.14,
          "angleDeg": 5.1
        }
      }
    }
  ]
}

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.

Continuous tour across a MapSet

get

Treats the pano-enabled maps in a MapSet as a single continuous tour, bridging between maps where they meet. Each node is identified by { mapCode, nodeId }; neighbours that cross into another map are marked cross: true. Positions are in the MapSet common frame.

Authorizations
AuthorizationstringRequired

JWT token obtained from the authentication endpoint.

Path parameters
idstringRequired

MapSet id or code.

Query parameters
mapCodesstringOptional

Comma-separated members (default all).

mapCodestringOptional

The current node's map (default primary).

nodeIdstringOptional
depthinteger · min: 1 · max: 4OptionalDefault: 2
bridgeRadiusMnumberOptional

Cross-map bridge radius (default = current map's minSpacingM).

Responses
200

Continuous cross-map window.

application/json
mapSetCodestringOptional
primaryMapCodestringOptional
bridgeRadiusMnumberOptional
depthintegerOptional
nodeCountintegerOptional
get/pano/map-set/{id}/window
GET /v1/pano/map-set/{id}/window HTTP/1.1
Host: api.multiset.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Accept: */*
{
  "mapSetCode": "text",
  "primaryMapCode": "text",
  "bridgeRadiusM": 1,
  "current": {
    "mapCode": "text",
    "nodeId": "text"
  },
  "depth": 1,
  "nodeCount": 1,
  "nodes": [
    {
      "nodeId": "000012",
      "keyframe": 12,
      "timeS": 4.5,
      "position": [
        1.2,
        1.6,
        -3.4
      ],
      "rotation": [
        0,
        0,
        0,
        1
      ],
      "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": "text",
      "neighbors": [
        {
          "mapCode": "text",
          "nodeId": "text",
          "distanceM": 1,
          "cross": true
        }
      ],
      "mapCode": "text"
    }
  ]
}

For large tours, prefer the window endpoints and load neighbours as the user moves, rather than fetching the full manifest up front.

Last updated

Was this helpful?