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
| Event | Fired when |
|---|---|
product.completed | A run finishes. The payload says what shipped - including a model that failed. |
product.failed | The 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.readyandmodel.failedare the scan-era event names. An endpoint may still subscribe to them, but nothing dispatches them - subscribe to theproduct.*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.
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_...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:
| Header | Value |
|---|---|
Viewrium-Signature | t=<unix-timestamp>,v1=<hex hmac-sha256> |
Viewrium-Event-Id | Unique id for this event (stable across any redelivery) |
Viewrium-Event-Type | product.completed or product.failed |
User-Agent | Viewrium-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
2xxquickly (do heavy work asynchronously), and use the deliveries list (below) to spot failures. TheViewrium-Event-Idis 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.
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)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
| Operation | SDK |
|---|---|
| List endpoints | webhooks.list() |
| Retrieve one | webhooks.retrieve(id) |
| Update (url, events, enabled) | webhooks.update(id, ...) |
| Delete | webhooks.delete(id) |
| List recent deliveries | webhooks.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.