Metadata-Version: 2.5
Name: viewrium
Version: 2.1.0
Summary: Official Python client for the Viewrium API: AR and 3D product visualization for furniture retailers.
Project-URL: Homepage, https://www.viewrium.com
Project-URL: Documentation, https://www.viewrium.com/docs/sdks/python
Project-URL: API reference, https://www.viewrium.com/docs/api-reference
Author: Viewrium
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: 3d,api-client,ar,augmented-reality,ecommerce,furniture,glb,product-visualization,sdk,viewrium
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: httpx>=0.28
Requires-Dist: pydantic>=2.7
Description-Content-Type: text/markdown

# viewrium

The official Python SDK for the [Viewrium](https://www.viewrium.com) API.

Viewrium is AR and 3D product visualization for furniture retailers: it turns product photos, a
video, a product page URL or an existing 3D file into catalog images, a GLB model and an
embeddable 3D/AR viewer. This package is a typed, synchronous client for the Viewrium `/v1` REST
API: create products, review and publish them, export images, manage webhooks and API keys, and
read usage analytics.

- Documentation: https://www.viewrium.com/docs
- SDK reference: https://www.viewrium.com/docs/sdks/python
- Dashboard: https://www.viewrium.com

## Install

```bash
pip install viewrium
```

Or with uv:

```bash
uv add viewrium
```

Requires Python 3.12+. Requests and responses are pydantic v2 models.

## Authentication

Every request is authenticated with an API key, sent as `Authorization: Bearer <key>`. The SDK
sets the header for you.

- **Secret keys (`sk_`)** are for servers. They can do everything their scopes allow. Never ship
  one to a browser, a mobile app or a public repository. This SDK is meant for secret keys.
- **Publishable keys (`pk_`)** are safe in a browser but are origin-gated and limited to reading a
  single published product, minting its model ticket and sending analytics beacons. An
  organization with no allowed domains configured has its publishable keys refused everywhere. They power the
  [`<viewrium-viewer>` embed](https://www.viewrium.com/docs/embed), which is how a model is shown on
  a storefront.

Create keys in the dashboard under **Settings -> Advanced -> API keys**. A secret key is shown
exactly once, on create. See [Authentication](https://www.viewrium.com/docs/authentication) for
scopes.

```python
import os

from viewrium import Viewrium

client = Viewrium(os.environ["VIEWRIUM_API_KEY"])
```

Options:

```python
client = Viewrium(
    "sk_live_...",
    base_url="https://api.viewrium.com",  # default
    timeout=30.0,                          # seconds; the default
)
```

`products.create` runs the product analysis synchronously, which can take well over 30 seconds.
Pass a longer `timeout` (for example `timeout=300.0`) to a client that creates products.

The client is a context manager that closes its HTTP connection on exit:

```python
with Viewrium("sk_live_...") as client:
    page = client.products.list()
```

## Quickstart

`products.create` is the single generation entrypoint. Supply exactly one source: `image_urls`,
`video_url`, `source_url` (a product page) or `model_url` (an existing 3D file). Sources are URLs
that Viewrium fetches, so they must be publicly reachable.

A fresh request **parks**: it returns a `ProductParked` with the plan, the 3D model verdict, the
price in credits and a `resume_token`, and nothing is written. Posting the token back commits the
quote, starts the run and returns the `Product`.

```python
import os
import time

from viewrium import (
    ModelDimensions,
    ProductCreateRequest,
    ProductDraft,
    ProductParked,
    ProductUpdateRequest,
    Viewrium,
)

with Viewrium(os.environ["VIEWRIUM_API_KEY"], timeout=300.0) as client:
    # 1. Quote
    result = client.products.create(
        ProductCreateRequest(
            image_urls=[
                "https://cdn.example.com/chair-front.jpg",
                "https://cdn.example.com/chair-side.jpg",
            ],
            draft=ProductDraft(
                title="Oak dining chair",
                custom_id="sku-oak-chair",
                dimensions=ModelDimensions(height=92),
            ),
        )
    )

    # 2. Commit
    if isinstance(result, ProductParked):
        print(result.cost.total_credits, len(result.plan), result.model.allowed)
        result = client.products.create(ProductCreateRequest(resume_token=result.resume_token))
    assert not isinstance(result, ProductParked)  # a resume always commits

    # 3. Wait for the run (prefer webhooks in production)
    product = result
    while product.display_status == "generating":
        time.sleep(5)
        product = client.products.retrieve(product.id)
    print(product.display_status, product.model.status if product.model else None)

    # 4. Publish
    client.products.update(product.id, ProductUpdateRequest(published=True))
```

A resume may still edit the `draft` (title, price, tags, dimensions). It may not change the order
or the overrides: the quote is immutable, and changing what gets generated means a new request.
A `resume_token` expires after 7 days. A `model_url` source never parks; it commits straight away.

Only the fields you set on a request model are sent, so an explicit `None` (for example on an
image patch, where it clears an override) is different from leaving a field out.

See [How generation works](https://www.viewrium.com/docs/generation) and
[the quote and commit guide](https://www.viewrium.com/docs/guides/quote-and-commit).

## Resources

| Resource | Methods |
|---|---|
| `client.products` | `create`, `list`, `retrieve`, `update`, `delete`, `batch_delete`, `publish`, `tags`, `review`, `update_image`, `update_model`, `rescale`, `create_ticket`, `order`, `export` |
| `client.api_keys` | `list`, `create`, `revoke` |
| `client.usage` | `get` |
| `client.webhooks` | `list`, `create`, `retrieve`, `update`, `delete`, `list_deliveries` |
| `client.projects` | `list`, `retrieve` |
| `client.beacon` | `send` |
| `client` | `health()` |

Request models (`ProductCreateRequest`, `ProductDraft`, `ProductUpdateRequest`, `ProductFilters`,
`WebhookEndpointCreateRequest`, ...), response models (`Product`, `ProductParked`, `ModelPlan`,
`ProductCost`, `UsageResponse`, ...) and enums (`DisplayStatus`, `ListScope`, `ProductSortField`,
`SortOrder`, `UsageEventType`, ...) are all importable from the top-level `viewrium` package.

### Showing a model: tickets

No response carries a URL for a 3D model file - not the product, not a webhook. The only way to a
model's bytes is a **ticket**: a single-use URL that serves the GLB once and expires within a
minute (60 s for `view`, 30 s for `ar`). Mint one right before the viewer loads and hand the URL
straight to the page; never store it.

```python
from viewrium import TicketKind, TicketRequest

ticket = client.products.create_ticket(
    product_id,
    # the page that will load the model; one of your allowed domains
    TicketRequest(kind=TicketKind.VIEW, origin="https://shop.example.com"),
)
print(ticket.url, ticket.expires_at)
```

A `view` ticket streams only to a request from its `origin`. An `ar` ticket is for Android's Scene
Viewer, which downloads the file outside the page: mint it on the AR tap. Minting is rate limited
per IP and per product (429).

### Listing and filtering

The catalog filters are one `ProductFilters` object, shared by `list`, `tags` and `export`. Paging
and sorting are keyword arguments on `list`.

```python
from viewrium import DisplayStatus, ProductFilters, ProductSortField, SortOrder

page = client.products.list(
    ProductFilters(status=[DisplayStatus.PUBLISHED], tag=["chairs"], q="oak"),
    limit=50,
    sort=ProductSortField.CREATED_AT,
    order=SortOrder.DESC,
)
for product in page.data:
    print(product.id, product.title, product.display_status)
```

### Exporting images

`products.export` returns a ZIP of finished images as bytes (the 3D model is never included).
When more images need finishing than one request can do, it returns a `ProductExportQueued`
instead: the images are finished in the background, a download link is emailed, and the same call
returns the archive once they are done.

```python
from viewrium import ProductExportQueued, ProductFilters

archive = client.products.export(ProductFilters(tag=["chairs"]))
if isinstance(archive, ProductExportQueued):
    print("finishing", archive.to_finish, "images; a link goes to", archive.notify_email)
else:
    with open("catalog.zip", "wb") as f:
        f.write(archive)
```

### Analytics

```python
from datetime import UTC, datetime, timedelta

end = datetime.now(UTC)
usage = client.usage.get(
    start=end - timedelta(days=90),
    end=end,
    granularity="day",
    group_by="product",
)
for row in usage.by_product:
    print(row.title, row.totals)
```

Traffic from your own team is excluded unless you pass `include_internal=True`.

## Errors

Non-2xx responses raise a `ViewriumError` subclass. Every error exposes `status`, `code`,
`message` and `details` from the API's error envelope.

```python
from viewrium import (
    APIConnectionError,
    AuthenticationError,
    ConflictError,
    PaymentRequiredError,
    ProductCreateRequest,
    RateLimitError,
    ViewriumError,
)

try:
    client.products.create(ProductCreateRequest(image_urls=["https://cdn.example.com/chair.jpg"]))
except AuthenticationError:
    ...  # 401: missing or invalid key
except PaymentRequiredError as err:
    ...  # 402: the plan does not allow this; err.details["reason"] says why
except ConflictError:
    ...  # 409: e.g. a duplicate custom_id or a replayed resume_token
except RateLimitError:
    ...  # 429: rate limited, or code "quota_exceeded" when credits run out
except APIConnectionError:
    ...  # the request never reached the API, or timed out
except ViewriumError as err:
    print(err.status, err.code, err.message, err.details)
```

| HTTP | Exception |
|---|---|
| 400 | `BadRequestError` |
| 401 | `AuthenticationError` |
| 402 | `PaymentRequiredError` |
| 403 | `PermissionDeniedError` |
| 404 | `NotFoundError` |
| 409 | `ConflictError` |
| 422 | `UnprocessableEntityError` |
| 429 | `RateLimitError` |
| 5xx | `ServerError` |
| none | `APIConnectionError` |

The SDK does not retry. On a `RateLimitError`, back off and retry with an exponential delay.

## Links

- [Quickstart](https://www.viewrium.com/docs/quickstart)
- [Python SDK reference](https://www.viewrium.com/docs/sdks/python)
- [Webhooks](https://www.viewrium.com/docs/webhooks)
- [API reference](https://www.viewrium.com/docs/api-reference)
- Support: support@viewrium.com

## License

Apache-2.0
