curl --request POST \
--url https://apis.viggle.ai/v1/scenes/pano \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sessionId": "living-room-01",
"prompt": "<string>",
"referenceImages": [
"<string>"
],
"quality": "low",
"panoramaUri": "<string>",
"spzOutputUrl": "<string>",
"outputUris": {
"spz": "<string>",
"preview": "<string>",
"panorama": "<string>"
}
}
'import requests
url = "https://apis.viggle.ai/v1/scenes/pano"
payload = {
"sessionId": "living-room-01",
"prompt": "<string>",
"referenceImages": ["<string>"],
"quality": "low",
"panoramaUri": "<string>",
"spzOutputUrl": "<string>",
"outputUris": {
"spz": "<string>",
"preview": "<string>",
"panorama": "<string>"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
sessionId: 'living-room-01',
prompt: '<string>',
referenceImages: ['<string>'],
quality: 'low',
panoramaUri: '<string>',
spzOutputUrl: '<string>',
outputUris: {spz: '<string>', preview: '<string>', panorama: '<string>'}
})
};
fetch('https://apis.viggle.ai/v1/scenes/pano', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://apis.viggle.ai/v1/scenes/pano",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'sessionId' => 'living-room-01',
'prompt' => '<string>',
'referenceImages' => [
'<string>'
],
'quality' => 'low',
'panoramaUri' => '<string>',
'spzOutputUrl' => '<string>',
'outputUris' => [
'spz' => '<string>',
'preview' => '<string>',
'panorama' => '<string>'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://apis.viggle.ai/v1/scenes/pano"
payload := strings.NewReader("{\n \"sessionId\": \"living-room-01\",\n \"prompt\": \"<string>\",\n \"referenceImages\": [\n \"<string>\"\n ],\n \"quality\": \"low\",\n \"panoramaUri\": \"<string>\",\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://apis.viggle.ai/v1/scenes/pano")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sessionId\": \"living-room-01\",\n \"prompt\": \"<string>\",\n \"referenceImages\": [\n \"<string>\"\n ],\n \"quality\": \"low\",\n \"panoramaUri\": \"<string>\",\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/scenes/pano")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sessionId\": \"living-room-01\",\n \"prompt\": \"<string>\",\n \"referenceImages\": [\n \"<string>\"\n ],\n \"quality\": \"low\",\n \"panoramaUri\": \"<string>\",\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"sessionId": "<string>",
"operation": "pano",
"status": "processing",
"createdAt": 123,
"completedAt": 123,
"result": {
"operation": "pano",
"sessionId": "<string>",
"spz": {
"url": "<string>"
},
"preview": {
"url": "<string>"
},
"panorama": {
"url": "<string>"
},
"outputBatch": {
"batchId": "<string>",
"createdAt": 123,
"manifest": {
"url": "<string>"
},
"kind": "initial",
"views": [
{
"index": 1,
"url": "<string>",
"c2w": [
[
1,
0,
0,
0
],
[
0,
1,
0,
0
],
[
0,
0,
1,
0
],
[
0,
0,
0,
1
]
],
"intrinsic": [
123
]
}
]
},
"referenceViewCount": 123,
"newViewCount": 123,
"seconds": {}
},
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}Create Scene
Build a 3D Gaussian-splat scene from a text prompt or a 360° panorama.
curl --request POST \
--url https://apis.viggle.ai/v1/scenes/pano \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"sessionId": "living-room-01",
"prompt": "<string>",
"referenceImages": [
"<string>"
],
"quality": "low",
"panoramaUri": "<string>",
"spzOutputUrl": "<string>",
"outputUris": {
"spz": "<string>",
"preview": "<string>",
"panorama": "<string>"
}
}
'import requests
url = "https://apis.viggle.ai/v1/scenes/pano"
payload = {
"sessionId": "living-room-01",
"prompt": "<string>",
"referenceImages": ["<string>"],
"quality": "low",
"panoramaUri": "<string>",
"spzOutputUrl": "<string>",
"outputUris": {
"spz": "<string>",
"preview": "<string>",
"panorama": "<string>"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
sessionId: 'living-room-01',
prompt: '<string>',
referenceImages: ['<string>'],
quality: 'low',
panoramaUri: '<string>',
spzOutputUrl: '<string>',
outputUris: {spz: '<string>', preview: '<string>', panorama: '<string>'}
})
};
fetch('https://apis.viggle.ai/v1/scenes/pano', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://apis.viggle.ai/v1/scenes/pano",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'sessionId' => 'living-room-01',
'prompt' => '<string>',
'referenceImages' => [
'<string>'
],
'quality' => 'low',
'panoramaUri' => '<string>',
'spzOutputUrl' => '<string>',
'outputUris' => [
'spz' => '<string>',
'preview' => '<string>',
'panorama' => '<string>'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://apis.viggle.ai/v1/scenes/pano"
payload := strings.NewReader("{\n \"sessionId\": \"living-room-01\",\n \"prompt\": \"<string>\",\n \"referenceImages\": [\n \"<string>\"\n ],\n \"quality\": \"low\",\n \"panoramaUri\": \"<string>\",\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://apis.viggle.ai/v1/scenes/pano")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"sessionId\": \"living-room-01\",\n \"prompt\": \"<string>\",\n \"referenceImages\": [\n \"<string>\"\n ],\n \"quality\": \"low\",\n \"panoramaUri\": \"<string>\",\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/scenes/pano")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"sessionId\": \"living-room-01\",\n \"prompt\": \"<string>\",\n \"referenceImages\": [\n \"<string>\"\n ],\n \"quality\": \"low\",\n \"panoramaUri\": \"<string>\",\n \"spzOutputUrl\": \"<string>\",\n \"outputUris\": {\n \"spz\": \"<string>\",\n \"preview\": \"<string>\",\n \"panorama\": \"<string>\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"sessionId": "<string>",
"operation": "pano",
"status": "processing",
"createdAt": 123,
"completedAt": 123,
"result": {
"operation": "pano",
"sessionId": "<string>",
"spz": {
"url": "<string>"
},
"preview": {
"url": "<string>"
},
"panorama": {
"url": "<string>"
},
"outputBatch": {
"batchId": "<string>",
"createdAt": 123,
"manifest": {
"url": "<string>"
},
"kind": "initial",
"views": [
{
"index": 1,
"url": "<string>",
"c2w": [
[
1,
0,
0,
0
],
[
0,
1,
0,
0
],
[
0,
0,
1,
0
],
[
0,
0,
0,
1
]
],
"intrinsic": [
123
]
}
]
},
"referenceViewCount": 123,
"newViewCount": 123,
"seconds": {}
},
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}sessionId and either a prompt or a panoramaUri. With a prompt, the scene model first generates a 360° panorama — optionally guided by reference images — and then builds the scene from it; with panoramaUri, it builds the scene from your panorama. The request is asynchronous: it answers immediately with a processing Scene Task whose ID starts with scn_, and you poll Get Scene Task until it finishes.
A ready pano task gives you the scene as an .spz Gaussian splat, the panorama, and an initial image batch of posed views. Pass that batch’s batchId to Generate Scene Views and Reconstruct Scene to extend the scene.
Sessions
Every scene request names asessionId of your choosing. A session holds one scene and every image batch made for it:
sessionIdis 1–128 ASCII letters, digits, underscores, or hyphens. It is scoped to your account, so it can’t collide with another customer’s.- A session runs one task at a time. Submitting while a task is still
processinganswers409with that task’s ID inerror.details.running_task_id— poll it, then submit again. referenceBatchIdsongenerateandreconstructonly accept batches made in the same session; a batch from another session fails the task withINVALID_REQUEST.
Request parameters
Sendapplication/json, up to 1 MiB. Unknown fields are rejected.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
sessionId | string | Yes | — | Session to build the scene in — see Sessions. |
prompt | string | One of prompt / panoramaUri | — | Description of the scene, 1–1000 characters, not blank. The panorama is generated from it. |
referenceImages | string[] | No | [] | Up to 8 image URLs that guide panorama generation. Only with prompt. |
quality | string | No | low | Panorama generation quality: fast, low, medium, or high. Only with prompt. |
panoramaUri | string | One of prompt / panoramaUri | — | URL of an existing 360° panorama image to build the scene from. Can’t be combined with prompt, referenceImages, or quality. |
spzOutputUrl | string | No | — | Your own upload URL for the .spz scene — see Upload to your own storage. |
outputUris | object | No | — | Your own upload URLs per file: spz, preview, panorama. Can’t be combined with spzOutputUrl. |
URL rules
panoramaUri, referenceImages, spzOutputUrl, and outputUris are fetched or written by Viggle’s servers, so each must be:
httporhttps, without a username or password, with a valid port if one is given;- on a host that resolves only to public internet addresses. Private, loopback, link-local, and carrier-grade NAT addresses are rejected with
400.
Upload to your own storage
spzOutputUrl, or outputUris.spz / outputUris.preview / outputUris.panorama, are URLs Viggle uploads the finished file to with an HTTP PUT — typically presigned upload URLs from S3, R2, or GCS. They are optional; without them, download the files from the task result while it is readable.
spzOutputUrlis the same asoutputUris.spz; send one or the other.- An
.spzsent to your URL is not kept by Viggle: the result’sspz.urlis your URL. - A preview or panorama sent to your URL is also kept by Viggle, and the result still links to Viggle’s signed copy.
- A
nullentry inoutputUrisis ignored. The same fields are accepted by every scene operation.
Response parameters
Returns200 OK with a Scene Task in processing state — the same shape Get Scene Task returns:
{
"id": "scn_0199f0c2-6a1e-7c3d-9b2a-4f5e6d7c8b9a.living-room-01",
"sessionId": "living-room-01",
"operation": "pano",
"status": "processing",
"createdAt": 1791856740000,
"completedAt": null,
"result": null,
"error": null
}
Common errors
| Error | Cause |
|---|---|
400 INVALID_REQUEST | A field is missing, invalid, or unknown, or a URL breaks the URL rules. message names the field. No task starts. |
401 UNAUTHENTICATED | Missing or invalid API key. |
409 INVALID_REQUEST | The session already has a task processing. error.details.running_task_id is that task; poll it before submitting again. |
503 SERVICE_BUSY | The scene model is temporarily unavailable. Retryable. |
Examples
curl -X POST https://apis.viggle.ai/v1/scenes/pano \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "living-room-01",
"prompt": "A sunlit Scandinavian living room with a grey sofa and large windows",
"quality": "low"
}'
import os, requests
response = requests.post(
"https://apis.viggle.ai/v1/scenes/pano",
headers={"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"},
json={
"sessionId": "living-room-01",
"prompt": "A sunlit Scandinavian living room with a grey sofa and large windows",
"quality": "low",
},
)
response.raise_for_status()
task = response.json()
const response = await fetch("https://apis.viggle.ai/v1/scenes/pano", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VIGGLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
sessionId: "living-room-01",
prompt: "A sunlit Scandinavian living room with a grey sofa and large windows",
quality: "low",
}),
});
const task = await response.json();
From an existing panorama
curl -X POST https://apis.viggle.ai/v1/scenes/pano \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "courtyard-01",
"panoramaUri": "https://example.com/courtyard-360.jpg"
}'
With reference images and your own storage
curl -X POST https://apis.viggle.ai/v1/scenes/pano \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "loft-01",
"prompt": "An industrial loft with exposed brick and warm evening light",
"referenceImages": ["https://example.com/loft-a.jpg", "https://example.com/loft-b.jpg"],
"quality": "high",
"outputUris": {
"spz": "https://my-bucket.s3.amazonaws.com/scenes/loft-01.spz?X-Amz-Signature=...",
"panorama": "https://my-bucket.s3.amazonaws.com/scenes/loft-01.png?X-Amz-Signature=..."
}
}'
Next step
Get Scene Task
Quick Start: Scene Model
Authorizations
Server-side SDK clients use a project API key. Remote MCP clients use an OAuth access token. Never expose a project API key in browser code.
Headers
Optional caller-supplied correlation ID, up to 128 characters. The service returns the effective value in the response header for tracing and support.
1 - 128Optional source-channel label, up to 128 characters, used to attribute traffic to an SDK, integration, product surface, or internal workflow.
1 - 128Body
Body of POST /v1/scenes/pano. Send either prompt (optionally
with referenceImages and quality) or panoramaUri, not both.
spzOutputUrl and outputUris are mutually exclusive. Input URLs
must be http(s), without credentials, on a host that resolves
only to public addresses.
Session chosen by the caller, scoped to the caller's account. A
session holds one scene and its image batches, and runs one task at
a time: a submission while a task is still processing answers
409 with that task in error.details.running_task_id.
1 - 128^[A-Za-z0-9_-]+$"living-room-01"
Scene description; a 360° panorama is generated from it. Must not be blank.
1 - 1000Image URLs that guide panorama generation. Only with prompt.
8Panorama generation quality. Only with prompt.
fast, low, medium, high URL of an existing 360° panorama to build the scene from. Not combinable with prompt, referenceImages, or quality.
Same as outputUris.spz.
The caller's own upload URLs; Viggle PUTs each finished file to
its URL. Null entries are ignored. An .spz sent here is not kept
by Viggle, and the result's spz.url is this URL; a preview or
panorama sent here is also kept, and the result links to Viggle's
copy. Each URL must be http(s), without credentials, on a host
that resolves only to public addresses.
Show child attributes
Show child attributes
Response
Scene task started.
A scene task (scn_ prefix), returned by every /v1/scenes
submission and by GET /v1/scenes/{task_id}. ready, failed, and
cancelled are terminal. A task stays readable for about 72 hours
after it finishes.
Task ID, scn_{runId}.{sessionId}.
A custom-pano task reports pano.
pano, generate, reconstruct processing, ready, failed, cancelled Start time, Unix milliseconds.
End time, Unix milliseconds; null while processing.
Set only when ready.
Show child attributes
Show child attributes
Set only when failed. code is INVALID_REQUEST (not
retryable — the model rejected the input, such as a batch from
another session) or PROCESSING_FAILED (retryable).
Show child attributes
Show child attributes

