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) -
statusis"parked". No product was created and nothing was persisted. The response carries aresume_tokenand 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 themodel_urlpath. 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
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"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.kindiswantedorextra,statusisplannedorskipped, andreason_codeis a closed vocabulary with routing's own factualreasonbeside it. Write your customer-facing copy from the code, never from the sentence.deliverable: falsemarks a row the 3D model needs and nobody is billed for.flags- what the analysis saw:transparent,geometry_risk,texture_risk, andsource_quality.reasonsare its own words; show them verbatim.source_quality: "poor"warns and gates nothing.model- the verdict.unseen_viewsnames every viewpoint that would be invented;blocked_bysays 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.
product = client.products.create(
ProductCreateRequest(
resume_token=result.resume_token,
draft=ProductDraft(title="Oak dining chair, natural", price=249.0),
)
)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.
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),
)
)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_tokenexpires. 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_urlpath never parks - supplying a finished model always commits immediately. - Discriminate carefully. In TypeScript, check
result.status === "parked". In Python,isinstance(result, ProductParked)(or the samestatuscheck) tells the two results apart.