JavaScript SDK
Install and use @viewrium/sdk, the official typed TypeScript client for the /v1 API.
@viewrium/sdk is the official JavaScript/TypeScript SDK - an ergonomic, fully typed client for the
Viewrium /v1 API. It ships ESM and CommonJS builds with self-contained type declarations, and runs
anywhere there is a fetch (Node 18+, Bun, Deno, edge runtimes, the browser).
Install
npm install @viewrium/sdkClient
import { Viewrium } from "@viewrium/sdk";
const client = new Viewrium({ apiKey: "sk_live_..." });Options:
const client = new Viewrium({
apiKey: "sk_live_...",
baseUrl: "https://api.viewrium.com", // default; trailing slash stripped
fetch: customFetch, // optional; defaults to globalThis.fetch
});There is also a createViewrium({ apiKey }) factory that is equivalent to new Viewrium(...).
Use a secret (
sk_) key only on a server. For browser viewers, use the<viewrium-model>embed with a publishable (pk_) key instead of the SDK.
Resources
| Resource | Methods |
|---|---|
client.products | create, list, retrieve, update, delete, batchDelete, publish, tags, review, updateImage, updateModel, rescale, order, export |
client.apiKeys | list, create, revoke |
client.usage | get |
client.webhooks | list, create, retrieve, update, delete, listDeliveries |
client.projects | list, retrieve |
client.beacon | send |
client | health() |
Request and response types (ProductCreateParams, Product, ProductCreateResult,
ProductParked, PlannedView, ModelPlan, ProductCost, UsageProductTotals, and so on) are
exported and derived directly from the API's OpenAPI schema.
products.create resolves to two shapes
create resolves to a ProductCreateResult - the parked quote for a fresh request, or the
committed product once you post the token back. Discriminate on status:
const result = await client.products.create({
image_urls: ["https://cdn.example.com/chair.jpg"],
draft: { title: "Oak dining chair" },
});
if (result.status === "parked") {
console.log(result.cost.total_credits, result.model.unseen_views);
const product = await client.products.create({ resume_token: result.resume_token });
if (product.status === "parked") throw new Error("a resume always commits");
console.log(product.id, product.display_status);
}See the verification gate guide for the whole flow.
Listing and filtering
const page = await client.products.list({
scope: "org",
status: ["published"],
tag: ["chairs"],
tag_match: "all",
q: "oak",
custom_id: "sku-123",
limit: 50,
offset: 0,
sort: "created_at",
order: "desc",
});
for (const product of page.data) {
console.log(product.id, product.title, product.display_status);
}products.tags and products.export take the same filters.
Analytics
usage.get takes an arbitrary window, not a preset. group_by: "product" additionally returns the
window's best-performing products, ranked by 3D views then AR entries and capped at 20, each with
its title.
const usage = await client.usage.get({
start: "2026-06-01T00:00:00Z",
end: "2026-09-01T00:00:00Z",
granularity: "day", // or "hour"
event_type: "view",
include_internal: false, // your own team's traffic is excluded by default
group_by: "product",
});
for (const row of usage.by_product ?? []) {
console.log(row.title, row.totals);
}The export archive
products.export resolves to the ZIP as a Blob:
const archive = await client.products.export({ tag: ["chairs"], include: ["images", "model"] });Errors
Non-2xx responses throw a typed ViewriumError subclass. Branch with instanceof:
import {
Viewrium,
ViewriumError,
AuthenticationError,
ConflictError,
RateLimitError,
ConnectionError,
} from "@viewrium/sdk";
const client = new Viewrium({ apiKey: "sk_live_..." });
try {
await client.products.create({ image_urls: ["https://cdn/chair.jpg"] });
} catch (err) {
if (err instanceof AuthenticationError) {
// 401
} else if (err instanceof ConflictError) {
// 409, e.g. duplicate custom_id
} else if (err instanceof RateLimitError) {
// 429 - back off and retry
} else if (err instanceof ConnectionError) {
// never reached the API
} else if (err instanceof ViewriumError) {
console.log(err.status, err.code, err.message, err.details);
}
}The full status-to-error table is on the Authentication page.
Full example
import { Viewrium } from "@viewrium/sdk";
const client = new Viewrium({ apiKey: "sk_live_..." });
const quote = await client.products.create({
image_urls: ["https://cdn.example.com/chair.jpg"],
draft: { title: "Oak dining chair", custom_id: "sku-oak-chair", dimensions: { height: 92 } },
});
if (quote.status !== "parked") throw new Error("unexpected: already committed");
const committed = await client.products.create({ resume_token: quote.resume_token });
if (committed.status === "parked") throw new Error("a resume always commits");
let product = await client.products.retrieve(committed.id);
while (product.display_status === "generating") {
await new Promise((r) => setTimeout(r, 5000));
product = await client.products.retrieve(product.id);
}
console.log(product.display_status, product.model?.glb_url);
await client.products.update(product.id, { published: true });