Viewrium Docs
Guides

The verification gate

Handle the parked quote, read the plan and the model verdict, and commit with resume_token.

Before anything is generated - or charged - Viewrium analyzes the request: it acquires the source media, reads the product, plans the catalog positions, and prices them. Then it parks. This one flow is the only non-obvious part of the API, so it is worth walking through.

A fresh create always parks

products.create resolves to exactly one of:

  • A parked quote (HTTP 200) - status is "parked". No product was created and nothing was persisted. The response carries a resume_token and everything you need to decide: the plan, the analysis flags, the model verdict, and the cost.
  • A committed product (HTTP 202) - you passed a resume_token, or you used the model_url path. The product and its asset rows exist and the run has started.

Nothing can orphan, because nothing exists until you commit: the whole analysis lives inside the token, which round-trips to you and back.

{
  "status": "parked",
  "resume_token": "...",
  "expires_at": "2026-09-23T12:34:56Z",
  "form": "seating",
  "title": "Oak dining chair",
  "flags": {
    "transparent": false,
    "geometry_risk": false,
    "texture_risk": false,
    "source_quality": "ok",
    "reasons": []
  },
  "plan": [{ "view": "front", "kind": "wanted", "status": "planned", "deliverable": true }],
  "model": { "ordered": true, "allowed": true, "blocked_by": [], "views": [], "unseen_views": ["back"] },
  "cost": { "images": 4, "image_credits": 4, "model_credits": 10, "total_credits": 14 }
}

Show the plan, the cost and the flags to a human (or apply your own auto-approve logic), then commit by calling products.create again with the resume_token.

Full flow

Python
from viewrium import ProductCreateRequest, ProductDraft, ProductParked, Viewrium

client = Viewrium("sk_live_...")

# 1. Quote. Nothing is written.
result = client.products.create(
    ProductCreateRequest(
        image_urls=["https://cdn.example.com/chair.jpg"],
        draft=ProductDraft(title="Oak dining chair"),
    )
)

if isinstance(result, ProductParked):
    print("Will produce:", [row.view for row in result.plan])
    print("Costs:", result.cost.total_credits, "credits")
    for reason in result.flags.reasons:
        print("Flag:", reason.sentence)          # shown verbatim
    if result.model.unseen_views:
        print("Invented viewpoints:", result.model.unseen_views)

    # ...show this to a user and get their OK...

    # 2. Commit. This is the call that writes and starts the run.
    product = client.products.create(
        ProductCreateRequest(resume_token=result.resume_token)
    )
    assert not isinstance(product, ProductParked)   # a resume always commits
else:
    product = result            # model_url: committed straight away

print(product.id, product.display_status)   # -> "<uuid>", "generating"
TypeScript
import { Viewrium } from "@viewrium/sdk";

const client = new Viewrium({ apiKey: "sk_live_..." });

// 1. Quote. Nothing is written.
const result = await client.products.create({
  image_urls: ["https://cdn.example.com/chair.jpg"],
  draft: { title: "Oak dining chair" },
});

// 2. Discriminate on `status`.
let product;
if (result.status === "parked") {
  console.log(result.cost.total_credits, result.model.unseen_views);
  for (const reason of result.flags.reasons ?? []) console.log(reason.sentence);

  // ...show this to a user and get their OK...

  product = await client.products.create({ resume_token: result.resume_token });
  if (product.status === "parked") throw new Error("a resume always commits");
} else {
  product = result;             // model_url: committed straight away
}

console.log(product.id, product.display_status); // -> "<uuid>", "generating"

Reading the quote

  • plan - one row per page position, in page order. kind is wanted or extra, status is planned or skipped, and reason_code is a closed vocabulary with routing's own factual reason beside it. Write your customer-facing copy from the code, never from the sentence. deliverable: false marks a row the 3D model needs and nobody is billed for.
  • flags - what the analysis saw: transparent, geometry_risk, texture_risk, and source_quality. reasons are its own words; show them verbatim. source_quality: "poor" warns and gates nothing.
  • model - the verdict. unseen_views names every viewpoint that would be invented; blocked_by says why a model is refused. See the model verdict.
  • cost - image_credits + model_credits = total_credits (1 credit = $0.50: a photo is 1, a model 10). Non-deliverable rows are never priced.

What a resume may change

The quote is immutable. A resume may still edit the draft - title, description, price, currency, custom_id, tags, metadata, dimensions - because those never reached the plan. Stating a different order, or any overrides at all, is a 400: changing what gets generated means a new request, and a new quote.

Python
product = client.products.create(
    ProductCreateRequest(
        resume_token=result.resume_token,
        draft=ProductDraft(title="Oak dining chair, natural", price=249.0),
    )
)
TypeScript
const product = await client.products.create({
  resume_token: result.resume_token,
  draft: { title: "Oak dining chair, natural", price: 249 },
});

Taking a model the gate refused

overrides.force_model on the quote takes the model anyway; overrides.generate_unseen does the same for unseen catalog positions. Both are per-run answers: leave one unset and your organization's standing preference (Always generate models / Always generate images, in Settings -> Advanced) decides.

Python
from viewrium import ProductOverrides

result = client.products.create(
    ProductCreateRequest(
        image_urls=["https://cdn.example.com/chair.jpg"],
        overrides=ProductOverrides(force_model=True, generate_unseen=True),
    )
)
TypeScript
const result = await client.products.create({
  image_urls: ["https://cdn.example.com/chair.jpg"],
  overrides: { force_model: true, generate_unseen: true },
});

Things to know

  • resume_token expires. It is backed by short-lived pre-fetched source URLs (expires_at, 7 days). Resume promptly; do not persist tokens for later use.
  • A token commits once. Replaying one returns 409.
  • The model_url path never parks - supplying a finished model always commits immediately.
  • Discriminate carefully. In TypeScript, check result.status === "parked". In Python, isinstance(result, ProductParked) (or the same status check) tells the two results apart.

On this page