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.
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].keyframeandtimeS: the source video frame index and its timestamp in seconds. Present on tours built from 360 video. Absent on tours built from.e57or 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.rgbKeyis a single default tier for clients that don't select one. See Panorama images below.maskKey: optional person-mask image key (nullif 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", 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
+Xright,+Yup, and+Zback.Node frame: OpenGL/ARKit convention;
rotationmaps 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 = 0is the zenith (+90°) andv = 1the 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).
A paginated library of maps that have a virtual tour, newest first.
JWT token obtained from the authentication endpoint.
120Paginated list of pano-enabled maps.
8120Unauthorized. Invalid or expired token.
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.
Header/summary for one map's tour, including navigation hints when the map belongs to a version chain or MapSet.
JWT token obtained from the authentication endpoint.
Map id or map code.
MAP_ABC123DEF456Tour header.
MAP_ABC123DEF456Ground FloorMap thumbnail storage key.
Full-res equirect dimensions
4096x2048Equirect widths available for this tour, ascending.
[512,1024,4096]1240Unauthorized.
No pano data for this map.
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.
The tour header plus every node, sorted by nodeId. Load once to render a complete tour.
JWT token obtained from the authentication endpoint.
Map id or map code.
MAP_ABC123DEF456Header plus all nodes.
MAP_ABC123DEF456Ground FloorMap thumbnail storage key.
Full-res equirect dimensions
4096x2048Equirect widths available for this tour, ascending.
[512,1024,4096]1240Unauthorized.
No pano data for this map.
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 }.
Tour nodes in pages, for large tours.
JWT token obtained from the authentication endpoint.
Map id or map code.
MAP_ABC123DEF456150Paginated nodes.
Unauthorized.
No pano data for this map.
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
}
]
}
]
}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, default2).
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).
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.
JWT token obtained from the authentication endpoint.
Map id or map code.
MAP_ABC123DEF456Center on this node.
Graph hops from the center.
2World X (provide x
Orientation quaternion (provide all four
Window of nodes around the center.
Present when centered by position.
Present when centered by position + orientation.
Unauthorized.
No pano data for this map
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.
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.
JWT token obtained from the authentication endpoint.
Comma-separated version mapCodes (default all in chain).
Which version drives navigation (default base map).
2Same-view gate (default no cap).
Cross-version viewpoints.
Bad Request. MapCodes not in this version
Unauthorized.
No pano data for this version
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.
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.
JWT token obtained from the authentication endpoint.
MapSet id or code.
Comma-separated members (default all).
The current node's map (default primary).
2Cross-map bridge radius (default = current map's minSpacingM).
Continuous cross-map window.
Bad Request. MapCodes not in set
Unauthorized.
No pano member in this MapSet
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"
}
]
}Last updated
Was this helpful?

