> ## Documentation Index
> Fetch the complete documentation index at: https://docs.viggle.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Quick Start: Scene Model

> Turn a text prompt into a 3D Gaussian-splat scene, generate new views of it, and reconstruct it.

The scene model builds an explorable 3D scene — a Gaussian splat in `.spz` format — from a text prompt or a 360° panorama. This guide runs the whole loop:

1. **Create** a scene from a prompt with `POST /v1/scenes/pano`. You get the `.spz`, the generated panorama, and an `initial` batch of posed images.
2. **Generate** new images from a camera you choose with `POST /v1/scenes/generate`. They become a `generated` batch.
3. **Reconstruct** the scene from both batches with `POST /v1/scenes/reconstruct`, for an updated `.spz`.

Each step is an asynchronous task: it answers at once with a `scn_` task ID, and you poll `GET /v1/scenes/{task_id}` until it's `ready`.

Set `VIGGLE_API_KEY` and run the example below as-is — it needs no other input.

## What you need

* An API key from the [Viggle Dashboard](https://portal.viggle.ai/keys), exported as `VIGGLE_API_KEY`
* A text prompt, up to 1000 characters — or a public URL of a 360° panorama
* Credits: none — scene tasks don't currently consume credits

## Key ideas

* **Session.** Every request carries a `sessionId` you choose (1–128 letters, digits, `_`, or `-`). A session holds one scene and its image batches, and runs **one task at a time** — wait for a task to finish before submitting the next one in the same session.
* **Batch.** A set of posed images in a session. `pano` makes the `initial` batch, each `generate` makes a `generated` one. Their `batchId`s are what `generate` and `reconstruct` take in `referenceBatchIds`.
* **Camera.** Poses are 4 × 4 camera-to-world matrices (`c2w`) in the scene's frame. Every image in a batch comes with its `c2w`, so the simplest way to pick a new camera is to move an existing one.

## Build, extend, and reconstruct

The script creates a scene, saves `scene.spz` and `panorama.png`, generates new views from a camera moved 0.3 units sideways from the first view, then reconstructs and saves `scene-v2.spz`.

<CodeGroup>
  ```bash cURL theme={null}
  #!/usr/bin/env bash
  set -euo pipefail

  base="https://apis.viggle.ai/v1/scenes"
  auth="Authorization: Bearer $VIGGLE_API_KEY"
  session="quickstart-$(date +%s)"

  submit() { # operation, JSON body -> task ID
    curl -sf -X POST "$base/$1" -H "$auth" -H "Content-Type: application/json" -d "$2" | jq -r '.id'
  }

  wait_ready() { # task ID -> ready task JSON
    for _ in $(seq 1 360); do
      task=$(curl -sf "$base/$1" -H "$auth")
      case "$(echo "$task" | jq -r '.status')" in
        ready) echo "$task"; return 0 ;;
        failed|cancelled) echo "Task $1 ended: $(echo "$task" | jq -c '.error')" >&2; return 1 ;;
      esac
      sleep 5
    done
    echo "Timed out waiting for $1" >&2
    return 1
  }

  # 1. Create the scene from a prompt.
  task_id=$(submit pano "$(jq -n --arg s "$session" '{
    sessionId: $s,
    prompt: "A sunlit Scandinavian living room with a grey sofa and large windows",
    quality: "low"
  }')")
  pano=$(wait_ready "$task_id")
  curl -s -o scene.spz "$(echo "$pano" | jq -r '.result.spz.url')"
  curl -s -o panorama.png "$(echo "$pano" | jq -r '.result.panorama.url')"
  initial=$(echo "$pano" | jq -r '.result.outputBatch.batchId')
  echo "saved scene.spz and panorama.png; initial batch $initial"

  # 2. Generate views from the first camera, moved 0.3 units along its x axis.
  target=$(echo "$pano" | jq -c '.result.outputBatch.views[0].c2w | .[0][3] += 0.3')
  task_id=$(submit generate "$(jq -n --arg s "$session" --arg b "$initial" --argjson c "$target" '{
    sessionId: $s, referenceBatchIds: [$b], targetC2w: $c
  }')")
  generated=$(wait_ready "$task_id" | jq -r '.result.outputBatch.batchId')
  echo "generated batch $generated"

  # 3. Reconstruct from both batches.
  task_id=$(submit reconstruct "$(jq -n --arg s "$session" --arg a "$initial" --arg b "$generated" '{
    sessionId: $s, referenceBatchIds: [$a, $b]
  }')")
  curl -s -o scene-v2.spz "$(wait_ready "$task_id" | jq -r '.result.spz.url')"
  echo "saved scene-v2.spz"
  ```

  ```javascript Node theme={null}
  import { writeFile } from "node:fs/promises";

  const baseUrl = "https://apis.viggle.ai/v1/scenes";
  const headers = {
    Authorization: `Bearer ${process.env.VIGGLE_API_KEY}`,
    "Content-Type": "application/json",
  };
  const sessionId = `quickstart-${Date.now()}`;

  async function api(path, body) {
    const response = await fetch(`${baseUrl}${path}`, {
      method: body ? "POST" : "GET",
      headers,
      body: body && JSON.stringify(body),
    });
    const json = await response.json().catch(() => ({}));
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${JSON.stringify(json.error ?? json)}`);
    }
    return json;
  }

  async function waitReady(task) {
    for (let i = 0; i < 360 && task.status === "processing"; i++) {
      await new Promise((resolve) => setTimeout(resolve, 5000));
      task = await api(`/${task.id}`);
    }
    if (task.status !== "ready") {
      throw new Error(`Task ${task.id} ended ${task.status}: ${JSON.stringify(task.error)}`);
    }
    return task.result;
  }

  async function download(url, path) {
    const response = await fetch(url);
    await writeFile(path, Buffer.from(await response.arrayBuffer()));
    console.log(`saved ${path}`);
  }

  // 1. Create the scene from a prompt.
  const pano = await waitReady(await api("/pano", {
    sessionId,
    prompt: "A sunlit Scandinavian living room with a grey sofa and large windows",
    quality: "low",
  }));
  await download(pano.spz.url, "scene.spz");
  await download(pano.panorama.url, "panorama.png");
  const initial = pano.outputBatch.batchId;

  // 2. Generate views from the first camera, moved 0.3 units along its x axis.
  const targetC2w = structuredClone(pano.outputBatch.views[0].c2w);
  targetC2w[0][3] += 0.3;
  const generated = await waitReady(await api("/generate", {
    sessionId,
    referenceBatchIds: [initial],
    targetC2w,
  }));
  console.log(`generated ${generated.newViewCount} views in batch ${generated.outputBatch.batchId}`);

  // 3. Reconstruct from both batches.
  const rebuilt = await waitReady(await api("/reconstruct", {
    sessionId,
    referenceBatchIds: [initial, generated.outputBatch.batchId],
  }));
  await download(rebuilt.spz.url, "scene-v2.spz");
  ```

  ```python Python theme={null}
  import copy
  import os
  import time
  import requests

  base_url = "https://apis.viggle.ai/v1/scenes"
  headers = {"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"}
  session_id = f"quickstart-{int(time.time())}"


  def submit(operation, body):
      response = requests.post(f"{base_url}/{operation}", headers=headers, json=body)
      response.raise_for_status()
      return response.json()


  def wait_ready(task):
      for _ in range(360):
          if task["status"] != "processing":
              break
          time.sleep(5)
          response = requests.get(f"{base_url}/{task['id']}", headers=headers)
          response.raise_for_status()
          task = response.json()
      if task["status"] != "ready":
          raise RuntimeError(f"Task {task['id']} ended {task['status']}: {task.get('error')}")
      return task["result"]


  def download(url, path):
      response = requests.get(url)
      response.raise_for_status()
      with open(path, "wb") as f:
          f.write(response.content)
      print(f"saved {path}")


  # 1. Create the scene from a prompt.
  pano = wait_ready(submit("pano", {
      "sessionId": session_id,
      "prompt": "A sunlit Scandinavian living room with a grey sofa and large windows",
      "quality": "low",
  }))
  download(pano["spz"]["url"], "scene.spz")
  download(pano["panorama"]["url"], "panorama.png")
  initial = pano["outputBatch"]["batchId"]

  # 2. Generate views from the first camera, moved 0.3 units along its x axis.
  target_c2w = copy.deepcopy(pano["outputBatch"]["views"][0]["c2w"])
  target_c2w[0][3] += 0.3
  generated = wait_ready(submit("generate", {
      "sessionId": session_id,
      "referenceBatchIds": [initial],
      "targetC2w": target_c2w,
  }))
  print(f"generated {generated['newViewCount']} views in batch {generated['outputBatch']['batchId']}")

  # 3. Reconstruct from both batches.
  rebuilt = wait_ready(submit("reconstruct", {
      "sessionId": session_id,
      "referenceBatchIds": [initial, generated["outputBatch"]["batchId"]],
  }))
  download(rebuilt["spz"]["url"], "scene-v2.spz")
  ```
</CodeGroup>

Open `scene.spz` and `scene-v2.spz` in any viewer that reads SPZ Gaussian splats.

## Responses

Every submission answers immediately with a `processing` task:

```json theme={null}
{
  "id": "scn_0199f0c2-6a1e-7c3d-9b2a-4f5e6d7c8b9a.quickstart-1791856740",
  "sessionId": "quickstart-1791856740",
  "operation": "pano",
  "status": "processing",
  "createdAt": 1791856740000,
  "completedAt": null,
  "result": null,
  "error": null
}
```

Poll `GET /v1/scenes/{task_id}` until `status` is `ready`, `failed`, or `cancelled`. What a ready `result` holds depends on the operation:

| Operation | `spz` | `panorama` | `outputBatch` |
| - | :-: | :-: | - |
| `pano` | ✓ | ✓ | `initial` batch |
| `generate` | — | — | `generated` batch |
| `reconstruct` | ✓ | — | — |

Each `outputBatch.views[]` entry has the image `url`, its camera `c2w`, and `intrinsic`. Signed URLs last **3 hours** — fetch the task again for fresh ones — and a task stays readable for about **72 hours** after it finishes. See [Get Scene Task](/v1/api-reference/scenes/get) for every field.

## Start from your own panorama

Replace `prompt` with `panoramaUri` in step 1:

```json theme={null}
{ "sessionId": "courtyard-01", "panoramaUri": "https://example.com/courtyard-360.jpg" }
```

To place the panorama at a pose and scale in your own world — so every camera comes back in your coordinates — use [`POST /v1/scenes/custom-pano`](/v1/api-reference/scenes/custom-pano) with `panoramaC2w` and `metersPerUnit`.

## Save results to your own storage

Add `spzOutputUrl` — or `outputUris` with `spz`, `preview`, or `panorama` — to any request, and Viggle uploads those files to your URLs with `PUT`. Presigned S3, R2, or GCS upload URLs work. An `.spz` sent to your URL isn't kept by Viggle; the result's `spz.url` is then your URL. See [Upload to your own storage](/v1/api-reference/scenes/pano#upload-to-your-own-storage).

## If something fails

* **`409` on submit**: the session already has a task running. `error.details.running_task_id` names it; wait for it, then submit again.
* **Task `failed` with `INVALID_REQUEST`**: the model rejected the input, for example a batch from another session. Fix it before retrying.
* **Task `failed` with `PROCESSING_FAILED`, or `cancelled`**: submit the same request again.
* **`404` on poll**: wrong task ID, a task from another account, or past the 72-hour retention.

<CardGroup cols={2}>
  <Card title="Create Scene" icon="cube" href="/v1/api-reference/scenes/pano">
    Prompt, panorama, reference images, and quality.
  </Card>

  <Card title="Generate Scene Views" icon="camera" href="/v1/api-reference/scenes/generate">
    Cameras, projection, field of view, and seed.
  </Card>

  <Card title="Reconstruct Scene" icon="rotate" href="/v1/api-reference/scenes/reconstruct">
    Rebuild the `.spz` from your batches.
  </Card>

  <Card title="Get Scene Task" icon="magnifying-glass" href="/v1/api-reference/scenes/get">
    The task object, result fields, and errors.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.