Viewrium Docs

Webhooks

Receive HMAC-signed product.completed and product.failed events and verify their signatures.

A committed run is asynchronous. Instead of polling, register a webhook endpoint and Viewrium will POST to it the moment a run finishes. Every delivery is HMAC-signed so you can verify it came from Viewrium.

Events

EventFired when
product.completedA run finishes. The payload says what shipped - including a model that failed.
product.failedThe run itself failed. data.reason says why.

A failed model is not a failed product: the run completed, so it arrives as product.completed with model_status: "failed". Branch on model_status, not on the event.

model.ready and model.failed are the scan-era event names. An endpoint may still subscribe to them, but nothing dispatches them - subscribe to the product.* pair.

Register an endpoint

Create an endpoint in the dashboard, or with the API. Creating one returns a signing secret (prefixed whsec_) - store it; you use it to verify deliveries. An endpoint belongs to your organization.

Python
from viewrium import Viewrium, WebhookEndpointCreateRequest, WebhookEventType

client = Viewrium("sk_live_...")

endpoint = client.webhooks.create(
    WebhookEndpointCreateRequest(
        url="https://api.yourapp.com/hooks/viewrium",
        events=[WebhookEventType.PRODUCT_COMPLETED, WebhookEventType.PRODUCT_FAILED],
    )
)
print(endpoint.signing_secret)  # whsec_...
TypeScript
import { Viewrium } from "@viewrium/sdk";

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

const endpoint = await client.webhooks.create({
  url: "https://api.yourapp.com/hooks/viewrium",
  events: ["product.completed", "product.failed"],
});
console.log(endpoint.signing_secret); // whsec_...

The delivery

Viewrium sends a POST with a JSON body and these headers:

HeaderValue
Viewrium-Signaturet=<unix-timestamp>,v1=<hex hmac-sha256>
Viewrium-Event-IdUnique id for this event (stable across any redelivery)
Viewrium-Event-Typeproduct.completed or product.failed
User-AgentViewrium-Webhooks/1.0

The body is a JSON envelope:

{
  "id": "evt_...",
  "type": "product.completed",
  "created_at": "2026-09-16T12:34:56Z",
  "data": {
    "id": "0b1e...-product-uuid",
    "title": "Oak dining chair",
    "custom_id": "sku-oak-chair",
    "status": "awaiting_review",
    "display_status": "awaiting_review",
    "published": false,
    "images_ordered": true,
    "image_count": 4,
    "images": [{ "id": "...", "view": "front", "flagged": false }],
    "model_status": "ready",
    "model_url": "https://cdn.viewrium.com/.../model.glb",
    "thumbnail_url": "https://cdn.viewrium.com/.../thumb.jpg"
  }
}

data carries the public fields of the product the event is about - never a storage path, never the analysis. On product.failed it also carries a reason. Respond with any 2xx to acknowledge receipt.

Delivery is single-attempt - Viewrium does not currently retry a failed POST. Return 2xx quickly (do heavy work asynchronously), and use the deliveries list (below) to spot failures. The Viewrium-Event-Id is stable, so deduplicate on it if retries are added later.

Verify the signature

Compute an HMAC-SHA256 over the string "<timestamp>.<raw request body>" using your endpoint's signing secret, then compare it - in constant time - to the v1 value from the Viewrium-Signature header. Use the raw request body, exactly as received, not a re-serialized object.

Python
import hashlib
import hmac

def verify(secret: str, header: str, raw_body: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp, sent = parts["t"], parts["v1"]
    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, sent)
TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret: string, header: string, rawBody: string): boolean {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
  const { t: timestamp, v1: sent } = parts;
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(sent ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Reject the request (return 4xx) if verification fails. For extra safety against replay, also reject deliveries whose t timestamp is far from the current time.

Manage and debug

OperationSDK
List endpointswebhooks.list()
Retrieve onewebhooks.retrieve(id)
Update (url, events, enabled)webhooks.update(id, ...)
Deletewebhooks.delete(id)
List recent deliverieswebhooks.listDeliveries(id) / webhooks.list_deliveries(id)

The deliveries list records each attempt - HTTP status, timestamps, and any error - which is the fastest way to see why an endpoint is not receiving events.

On this page