Skip to main content
Documentation

Pano-to-VR

Package multiple panoramas, submit an overseas Pano-to-VR task, and poll for a browsable VR URL.

On this page

Powered by Argus.

Upload a ZIP containing multiple panoramas to create a Realsee project and retrieve a browsable VR URL through the overseas Pano-to-VR API.

The result includes a SaaS VR project_id and a browsable vr_url.

Before you start

  • Create or select a Team-owned Developer app in Apps.
  • Keep the app key, app secret, gateway access token, and task orchestration on your backend. Give temporary upload credentials only to the authenticated client uploading the ZIP.
  • Prepare equirectangular JPG panoramas and the ZIP manifest described below.
  • Install the uploader and its COS adaptor dependency:
npm install @realsee/universal-uploader@0.1.1 cos-js-sdk-v5@1.8.3

All requests in this guide use the overseas production gateway:

https://app-gateway.realsee.ai

ZIP package format

Create one ZIP with manifest.json at its root and the panorama files under images/:

my-project.zip
├── manifest.json
└── images/
    ├── entrance.jpg
    ├── entrance-02.jpg
    ├── end-of-nave.jpg
    ├── presbyterium.jpg
    └── high-altar.jpg

Example manifest.json:

{
  "version": "1.0",
  "floor_map": {
    "0": 1,
    "1": 2
  },
  "project_name": "my-pano-project",
  "scan_list": [
    { "id": "entrance", "floor": 0 },
    { "id": "entrance-02", "floor": 0 },
    { "id": "end-of-nave", "floor": 0 },
    { "id": "presbyterium", "floor": 1 },
    { "id": "high-altar", "floor": 1 }
  ]
}
FieldTypeDescription
versionstringPackage version. Use "1.0" for this workflow.
floor_mapobjectMaps each scan_list[].floor key to the displayed floor number.
project_namestringName recorded in the package. Use the same stable project name when submitting the task.
scan_listarrayOrdered panorama records.
scan_list[].idstringMust match the corresponding JPG filename without its extension.
scan_list[].floornumberFloor key represented in floor_map.

Every file under images/ must have a matching scan_list entry. For a single-floor project, one mapping such as { "0": 0 } is sufficient.

Workflow

Get access_token → get upload credentials → upload ZIP → submit task → poll task_code → read vr_url

Get an access token

Call POST /auth/access_token with application/x-www-form-urlencoded data:

app_key=<app_key>&app_secret=<app_secret>

A successful response contains data.access_token and data.expire_at. Send the raw token in the Authorization header on the remaining requests; do not add a Bearer prefix.

Get upload credentials

Call GET /open/v1/pano/file/token with the gateway token:

GET /open/v1/pano/file/token
Authorization: <raw_gateway_access_token>

This request returns the UploadToken fields directly under data.

Key response fields include:

FieldPurpose
tmpSecretId, tmpSecretKey, sessionTokenTemporary object-storage credentials. Never log them.
bucket, region, hostUpload destination.
prefixPrefix that must be prepended to the uploaded ZIP key.
ttl, expireCredential lifetime in seconds. Here expire is a TTL, not the expire_at Unix timestamp returned by authentication.
download_type, download_hostDetermine which file-location field to send when submitting the task.
custom_domain, custom_schemeProvider endpoint values used by the upload adaptor.

If the gateway returns code: -3, refresh the access token before requesting a new upload token.

Upload the ZIP from your client

The browser upload uses File and the COS adaptor. Pass the complete data object from the upload-token response as its UploadToken:

import {
  Uploader,
  type ProviderAdaptor,
  type UploadToken,
} from '@realsee/universal-uploader'

const cosAdaptor: ProviderAdaptor = () => import('@realsee/universal-uploader/adaptors/cos')

export async function uploadPanoramaZip(zipFile: File, token: UploadToken) {
  const uploader = new Uploader(cosAdaptor, {
    getToken: async () => token,
  })
  const uploadKey = 'my-project.zip'

  await uploader.upload(uploadKey, zipFile, {
    parallel: Uploader.defaultUploadHandler.parallel,
    partSize: Uploader.defaultUploadHandler.partSize,
    retry: Uploader.defaultUploadHandler.retry,
  })

  return `${token.prefix}${uploadKey}`
}

The returned string is the full object path used as private_cos_key. If download_host is non-empty and your upload adaptor returns a download_url, retain that URL for zip_cos_url instead.

Submit the processing task

Call POST /open/v1/pano/task/submit with project_name and one ZIP location:

  • Use private_cos_key when download_host is empty, which represents the private-bucket presign flow.
  • Use zip_cos_url when the upload result provides a downloadable URL.

Private-bucket example:

{
  "project_name": "my-pano-project",
  "private_cos_key": "vrfile/release/open_task_original/T-XXXXXXXXXX/pano/1700000000/my-project.zip"
}

Successful data contains both identifiers:

type PanoTaskSubmission = {
  project_id: string
  task_code: string
}

Retain task_code for this task's Pano-to-VR status queries.

Poll for completion

Call GET /open/v1/pano/task/status with the returned task code:

GET /open/v1/pano/task/status?task_code=<task_code>
Authorization: <raw_gateway_access_token>

The response data follows this schema:

type PanoTaskStatus = {
  project_id: string
  status: 'pending' | 'processing' | 'success' | 'failed'
  vr_url: string
}

Continue polling while the status is pending or processing. On success, vr_url contains the complete browsable Realsee URL. Stop and surface a retry or support path on failed.

The access token can expire during a long-running task. If the envelope returns code: -3 and status: "access_token expired", refresh the token and continue polling with the same task_code.

Response envelope

Pano-to-VR responses use the gateway envelope:

FieldDescription
request_id, trace_idIdentifiers to retain in server-side diagnostics.
business_codeString business result or error code.
code0 for success, -1 for a business failure, and -3 for an expired access token.
statusSuccess or error description.
dataEndpoint-specific data; it can be null on failure.
costServer processing time in milliseconds.

Do not expose the app secret, access token, temporary storage credentials, object path, or raw gateway diagnostics in browser logs or analytics.