# Pano-to-VR

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

Canonical: https://developer.realsee.ai/docs/pano-to-vr
Source: https://developer.realsee.ai/docs/pano-to-vr.md
Language: en

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](https://developer.realsee.ai/dashboard/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:

```bash
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:

```text
https://app-gateway.realsee.ai
```

## ZIP package format

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

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

Example `manifest.json`:

```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 }
  ]
}
```

| Field | Type | Description |
| --- | --- | --- |
| `version` | string | Package version. Use `"1.0"` for this workflow. |
| `floor_map` | object | Maps each `scan_list[].floor` key to the displayed floor number. |
| `project_name` | string | Name recorded in the package. Use the same stable project name when submitting the task. |
| `scan_list` | array | Ordered panorama records. |
| `scan_list[].id` | string | Must match the corresponding JPG filename without its extension. |
| `scan_list[].floor` | number | Floor 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

```text
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:

```text
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:

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

This request returns the `UploadToken` fields directly under `data`.

Key response fields include:

| Field | Purpose |
| --- | --- |
| `tmpSecretId`, `tmpSecretKey`, `sessionToken` | Temporary object-storage credentials. Never log them. |
| `bucket`, `region`, `host` | Upload destination. |
| `prefix` | Prefix that must be prepended to the uploaded ZIP key. |
| `ttl`, `expire` | Credential lifetime in seconds. Here `expire` is a TTL, not the `expire_at` Unix timestamp returned by authentication. |
| `download_type`, `download_host` | Determine which file-location field to send when submitting the task. |
| `custom_domain`, `custom_scheme` | Provider 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`:

```ts
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:

```json
{
  "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:

```ts
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:

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

The response data follows this schema:

```ts
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:

| Field | Description |
| --- | --- |
| `request_id`, `trace_id` | Identifiers to retain in server-side diagnostics. |
| `business_code` | String business result or error code. |
| `code` | `0` for success, `-1` for a business failure, and `-3` for an expired access token. |
| `status` | Success or error description. |
| `data` | Endpoint-specific data; it can be `null` on failure. |
| `cost` | Server 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.
