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

# Product rendering: images, spins and turntables

> Use model-preview controls, estimate rendering units, request proofs or final media, and publish approved immutable outputs.

## Goal

Render a validated product model with explicit usage acceptance while keeping delivery, governance and retained outputs native to StackShift.

## Prerequisites

* A current, clean, governed GLB of at most 10 MiB with an existing ready validated model package.
* Rendering enabled for your Assets space, with the desired output types available in its model workspace.
* A current Assets subscription whose purchased terms include rendering and enough remaining rendering units.

## Workflow

<Steps>
  <Step>
    Open the model inspector and inspect rendering.enabled, reason, qualified\_outputs, source and remaining.
  </Step>

  <Step>
    Adjust the camera, lighting and background in the local preview before requesting a proof.
  </Step>

  <Step>
    Estimate the exact source/recipe/output selection and present its units for explicit acceptance.
  </Step>

  <Step>
    Submit the same selection with an idempotency key and maximum\_units, then poll the returned set through the model endpoint.
  </Step>

  <Step>
    Review complete ready outputs, create a publication draft, and follow review/approval/publication before gallery delivery.
  </Step>
</Steps>

## Render from the dashboard

Open a validated GLB in Assets and find the rendering controls in its model inspector. If rendering is unavailable, read the displayed reason. Processing the model and rendering images are different steps; a current ready validated model is required before these controls can submit work.

Adjust camera orbit/elevation, zoom, exposure, shadow, background and environment in the local preview. These interactions do not submit a paid render. Neutral and Studio are the supported environment labels; the local preview approximates the result and is not the final image.

Choose Render preview to request a 256-pixel proof, or select the available Product image, Interactive spin and Turntable video outputs. Review the usage estimate and explicitly accept the rendering units before selecting Render preview or Generate outputs. Dragging the model viewer is for inspection; only the explicit slider settings are submitted. Changing settings after an estimate requires a new estimate for those settings.

You can leave the inspector while work finishes. Return to Rendered outputs, open Review outputs, and inspect each final image, the full spin or turntable. For a final ready set choose Create publication draft; a preview proof cannot be published. Follow the separate review/approval/publication steps before adding the media to a gallery.

## Boundaries and supported outputs

Cloudinary performs the authenticated 3D render. StackShift owns authorization, plan admission, durable jobs, immutable output storage, review, publication and delivery. Provider credentials and signed provider URLs must not reach browser responses.

The rendering API accepts four output types: proof, poster, spin and turntable. Use the model and gallery workflows to deliver interactive 3D, model animation and supplied AR files.

* proof: one 256 × 256 PNG, requested alone. It uses the poster output setting and is for preview; choose final outputs for publication.
* poster: one PNG at the recipe’s final size.
* spin: 24 separate PNG frames at 15-degree orbit increments, not a single animated image. Publication requires every unique ordered frame from the same set.
* turntable: one MP4 at the recipe’s final size. It requires a solid background and shadow=0 and uses one customer rendering unit.

## Recipe fields and accepted bounds

* environment: neutral or pillars; the customer-facing label Studio corresponds to pillars.
* orbit: -360 through 360 degrees, normalized into the canonical orbit range. elevation: -80 through 80 degrees.
* zoom: 0.5–2. exposure: 0–2. shadow: 0–1.
* background: transparent or exactly six hexadecimal digits without #. Turntable rejects transparent.
* size: 512 or 1024. Proof always uses 256 regardless of final recipe size.
* rendering.qualified\_outputs lists the supported final output types: poster, spin and turntable. Use that list to populate output controls; no separate launch allowlist is required.

## REST request and concurrency contract

GET /api/v1/assets/\{assetID}/models requires assets:read. Rendering commands reuse POST on that route with assets:process, X-Asset-Space-ID, and quoted If-Match for the current asset revision. The request body is limited to 16 KiB and unknown fields are rejected.

operation=estimate returns HTTP 200 with units, remaining, reused and optionally set\_id. It does not upload a source or submit a provider transformation. operation=render returns HTTP 202 with set\_id, status, optional job\_id/units and reused.

Every render requires an Idempotency-Key of 1–128 characters and maximum\_units accepted from an estimate. Bind the request to the exact version\_id and source\_checksum. On source/revision changes, refetch, re-estimate and obtain acceptance for the new selection.

```json theme={null}
{
  "operation": "estimate",
  "version_id": "YOUR_CURRENT_VERSION_UUID",
  "source_checksum": "YOUR_CURRENT_64_CHARACTER_SHA256",
  "recipe": {
    "environment": "neutral", "orbit": 0, "elevation": 15,
    "zoom": 1, "exposure": 1, "shadow": 0.5,
    "background": "transparent", "size": 512
  },
  "outputs": ["poster", "spin"]
}
```

## JavaScript SDK: estimate before submission

This backend example assumes stackshift has the selected assetSpaceId. Keep the returned selection and revision fixed while the user reviews the estimate. The acceptedMaximumUnits argument must come from that explicit acceptance, not an unbounded application default.

```ts theme={null}
import type { AssetRenderInput } from '@stackshift-cloud/sdk'

const selection: AssetRenderInput = {
  version_id: currentVersionId,
  source_checksum: currentSourceChecksum,
  recipe: {
    environment: 'neutral', orbit: 0, elevation: 15, zoom: 1,
    exposure: 1, shadow: 0.5, background: 'transparent', size: 512,
  },
  outputs: ['poster', 'spin'],
}
const estimate = await stackshift.assets.dam.estimateRender(
  assetId, currentAssetRevision, selection
)
// Present estimate.units and estimate.remaining before invoking this function.
async function submitAccepted(acceptedMaximumUnits: number, requestKey: string) {
  if (acceptedMaximumUnits !== estimate.units) {
    throw new Error('Accept the current estimate before rendering')
  }
  return stackshift.assets.dam.renderProduct(
    assetId, currentAssetRevision, requestKey, selection, acceptedMaximumUnits
  )
}
```

## Polling and reviewing a render set

Read assets.dam.models(assetId), then find the returned set\_id in rendering.sets. A set reports queued/processing/ready/failed, requested outputs, units, recipe, version\_id, timestamps, can\_retry, failure\_reason and immutable output references.

rendering.deliveries\[setId] supplies a short-lived authorized preview when available. Do not cache it or turn it into a permanent embed. A set is ready only after complete validated outputs exist in StackShift storage and provider-origin cleanup has been confirmed.

The inspector’s Review outputs displays the selected image, ordered spin or turntable. Create publication draft is offered for final outputs; proofs remain preview-only. Copy the returned references into the draft, then submit, review, approve and publish. Changing sliders creates a new request; it never mutates published output bytes.

## Rendering units, estimates and reuse

One proof or product-image PNG uses one rendering unit. A complete spin uses 24, so a product image plus spin uses 25. One turntable MP4 uses one unit. Use the returned estimate.units and estimate.remaining; these customer plan units are separate from Cloudinary credits and are not a monetary price.

A read-only estimate does not submit a render. Verified equivalent outputs can be reused; the response reports reused and may identify the existing set. Review reused outputs before publishing them. A new source version, changed recipe or different output selection can require new work.

Units are reserved on admission. Do not assume a failed request or month boundary automatically refunds them. If there is insufficient allowance or rendering is unavailable, the request must remain blocked until the account/platform owner resolves the condition; the browser must not bypass maximum\_units or invent an allowance.

## Retry without creating duplicate work

Keep the original request key and its exact source/recipe/output selection when retrying an uncertain client request. Read the current render set first. Do not create a new idempotency key simply because the browser timed out.

When a failed set reports can\_retry=true, resubmitting the same settings resumes incomplete collection without another render. When it is false, inspect the reason and obtain a fresh estimate and explicit acceptance before requesting a new set.

Changing the asset revision or source checksum requires a new read and an accepted selection for the current source. Never replay an old estimate against replacement bytes.

## Storage, publication and withdrawal

Ready outputs are validated and retained in StackShift storage. Rendering history and publication references identify immutable bytes; changing preview sliders cannot mutate a published image or spin.

Provider cleanup is managed by the service before readiness; customers do not need provider credentials or provider URLs in their embeds. Published output delivery remains under StackShift authorization.

Withdraw the publication or gallery when delivery must stop. Previously downloaded bytes cannot be recalled. For service operators, provider reservation and cleanup recovery procedures are documented separately in the Assets deployment and environment references.

## Troubleshooting in order

* rendering.enabled=false: read rendering.reason, confirm the selected space and current subscription allowance, and ask the platform owner about unavailable output types. Customers do not configure provider credentials in the browser.
* Ready-model prerequisite failure: open the model inspector, process the current GLB and wait for its ready package before requesting a render.
* Revision conflict or checksum mismatch: refetch the asset and pin a new accepted selection; do not reuse stale source identity.
* Turntable refused: use an opaque six-digit background and shadow=0, and check that turntable is included in the space’s available outputs.
* Failed with can\_retry=true: resume the same settings after restoring storage/provider access. Do not repeatedly generate new paid sets.
* Ready preview but publication rejected: exclude proof, include all 24 spin frames when requesting spin, and satisfy rights/channel/reviewer rules.
* For AR, publish the model with its required companion files and use the gallery’s AR action described in the model guide.

## Go, Python and PHP: prepare a source-bound estimate and submit after acceptance

These examples read rendering.source from the model workspace and the current revision from metadataEditor. The workspace uses camelCase source fields assetId and versionId plus checksum; the render request uses version\_id and source\_checksum. The selected recipe requests one product image and one complete spin.

Store the quote in your backend against the authenticated editor and product. Return only the displayable units and remaining allowance to your confirmation UI. After explicit acceptance, reload that stored quote and pass the accepted unit count plus a stable request key to submitProduct. Do not trust a browser-supplied replacement recipe, asset ID or revision.

Submission returns set\_id. Poll models and locate rendering.sets by that ID; queued and processing remain pending, failed exposes failure\_reason/can\_retry, and ready exposes output references. Retain the set ID if the client disconnects. Re-estimate after source/revision changes; never automatically accept a higher estimate.

<CodeGroup>
  ```go Go theme={null}
  package docsexample

  import (
  	"context"
  	"fmt"
  	stackshift "github.com/stackshiftCloud/assets-go"
  )

  type ProductQuote struct {
  	AssetID  string
  	Revision int64
  	Input    map[string]any
  	Estimate *stackshift.AssetRenderEstimate
  }

  func estimateProduct(ctx context.Context, dam *stackshift.AssetDAMClient, assetID string) (*ProductQuote, error) {
  	workspace, err := dam.Models(ctx, assetID)
  	if err != nil {
  		return nil, err
  	}
  	rendering, _ := workspace["rendering"].(map[string]any)
  	if rendering["enabled"] != true {
  		return nil, fmt.Errorf("rendering unavailable: %v", rendering["reason"])
  	}
  	source, _ := rendering["source"].(map[string]any)
  	editor, err := dam.MetadataEditor(ctx, assetID)
  	if err != nil {
  		return nil, err
  	}
  	revision, ok := editor["revision"].(float64)
  	if !ok {
  		return nil, fmt.Errorf("missing asset revision")
  	}
  	input := map[string]any{
  		"version_id": source["versionId"], "source_checksum": source["checksum"],
  		"recipe": map[string]any{"environment": "neutral", "orbit": 0, "elevation": 15,
  			"zoom": 1, "exposure": 1, "shadow": 0.5, "background": "transparent", "size": 512},
  		"outputs": []string{"poster", "spin"},
  	}
  	estimate, err := dam.EstimateRender(ctx, assetID, int64(revision), input)
  	if err != nil {
  		return nil, err
  	}
  	return &ProductQuote{AssetID: assetID, Revision: int64(revision), Input: input, Estimate: estimate}, nil
  }
  func submitProduct(ctx context.Context, dam *stackshift.AssetDAMClient, quote *ProductQuote, requestKey string, acceptedUnits int64) (*stackshift.AssetRenderSubmission, error) {
  	if quote == nil || quote.Estimate == nil || acceptedUnits != quote.Estimate.Units {
  		return nil, fmt.Errorf("accept the current estimate before rendering")
  	}
  	return dam.RenderProduct(ctx, quote.AssetID, quote.Revision, requestKey, quote.Input, acceptedUnits)
  }
  ```

  ```python Python theme={null}
  def estimate_product(dam, asset_id):
      rendering = dam.models(asset_id)["rendering"]
      if not rendering["enabled"]:
          raise RuntimeError(rendering["reason"])
      source = rendering["source"]
      revision = dam.metadata_editor(asset_id)["revision"]
      selection = {
          "version_id": source["versionId"], "source_checksum": source["checksum"],
          "recipe": {"environment": "neutral", "orbit": 0, "elevation": 15,
                     "zoom": 1, "exposure": 1, "shadow": 0.5, "background": "transparent", "size": 512},
          "outputs": ["poster", "spin"],
      }
      return {"asset_id": asset_id, "revision": revision, "selection": selection,
              "estimate": dam.estimate_render(asset_id, revision, selection)}

  def submit_product(dam, quote, request_key, accepted_units):
      if accepted_units != quote["estimate"]["units"]:
          raise ValueError("Accept the current estimate before rendering")
      return dam.render_product(quote["asset_id"], quote["revision"], request_key, quote["selection"], accepted_units)
  ```

  ```php PHP theme={null}
  function estimateProduct(\StackShift\AssetDAMClient $dam, string $assetId): array {
      $rendering = $dam->models($assetId)['rendering'];
      if (!$rendering['enabled']) { throw new \RuntimeException($rendering['reason']); }
      $source = $rendering['source'];
      $revision = $dam->metadataEditor($assetId)['revision'];
      $selection = [
          'version_id' => $source['versionId'], 'source_checksum' => $source['checksum'],
          'recipe' => ['environment' => 'neutral', 'orbit' => 0, 'elevation' => 15,
              'zoom' => 1, 'exposure' => 1, 'shadow' => 0.5, 'background' => 'transparent', 'size' => 512],
          'outputs' => ['poster', 'spin'],
      ];
      return ['asset_id' => $assetId, 'revision' => $revision, 'selection' => $selection,
          'estimate' => $dam->estimateRender($assetId, $revision, $selection)];
  }
  function submitProduct(\StackShift\AssetDAMClient $dam, array $quote, string $requestKey, int $acceptedUnits): array {
      if ($acceptedUnits !== $quote['estimate']['units']) {
          throw new \InvalidArgumentException('Accept the current estimate before rendering');
      }
      return $dam->renderProduct($quote['asset_id'], $quote['revision'], $requestKey, $quote['selection'], $acceptedUnits);
  }
  ```
</CodeGroup>

## Render result fields and publishing a complete spin

The estimate response contains units, remaining, reused and optional set\_id. The admission response contains set\_id and status, with job\_id/units for queued work or reused=true for an existing ready set. The model workspace supplies rendering.sets and rendering.deliveries keyed by set ID.

A set includes id, asset\_id, version\_id, source\_checksum, recipe, requested, status, units, outputs, failure\_reason when failed, can\_retry and timestamps. Each output contains role, frame and reference. Store set\_id and immutable reference objects; preview URLs are temporary and must be resolved again.

To publish poster plus spin, select the poster reference as role=media and all 24 frame references as role=spin\_frame, with spin\_frame values 0–23 from the same render\_set\_id. Preserve each reference’s asset\_id, version\_id, rendition\_id, render\_role and output\_checksum\_sha256. This produces 25 references, within the 30-reference publication limit. Do not select the spin’s frame-zero media alternative as a second primary.

For a spin-only publication, use frame zero as the sole role=media reference and frames 1–23 as spin\_frame. For a turntable publication, use the returned video reference as the sole primary and optionally include a poster companion. Use publication-renditions to obtain the available role variants; never construct checksums or IDs yourself.

## Expected result

<Check>
  An explicitly accepted render produces validated StackShift-owned outputs with reconciled reservations and provider cleanup; publishing and AR remain separate operations.
</Check>

## Common failures

<Warning>
  * Submitting from slider movement without an estimate and explicit acceptance.
  * Using a preview proof as a published product image.
  * Creating new request keys repeatedly after an uncertain response.
</Warning>

## Related guides

<CardGroup cols={2}>
  <Card title="Upload, validate and publish 3D models" href="/assets/models-ar-and-galleries">
    Prepare supported GLB and supplied USDZ files, inspect validation reports, add posters, review the model and launch authorized device AR.
  </Card>

  <Card title="Publications, review and usage rights" href="/assets/publications-review-and-rights">
    Select immutable renditions, capture metadata and rights, submit editorial review, publish by channel, and withdraw delivery.
  </Card>

  <Card title="Create and embed media galleries" href="/assets/media-galleries">
    Arrange published images, video, spins and models with locale, alternate text, approved fallbacks and stable published revisions.
  </Card>

  <Card title="Assets SDKs: JavaScript, Go, Python and PHP" href="/assets/sdk-media-workflows">
    Configure space-scoped clients and use native video, governed DAM, model rendering, galleries and CMS capabilities in all four SDKs.
  </Card>
</CardGroup>
