# Consultant Mode (/docs/consultant)
This section is under construction — Consultant Mode documentation is on its way.
Consultant Mode equips sales consultants for in-home appointments: photograph the
customer's space, mark out the areas to cover, and present product and material options
as photorealistic visualisations — live, in the room where the products will hang.
# CRM Integration (/docs/crm)
This section is under construction — CRM integration documentation is on its way.
Viewa connects to your CRM so that visualisations, selections, and customer activity
flow into the systems your sales team already works in.
# Agent Skill (/docs/ecommerce/agent-skill)
**In progress.** The skill ships alongside the API's general availability — the
walkthrough sections for the widget and raw-API paths are finalised against the
live surface before release.
The fastest way to integrate Viewa is to let your coding agent do it. The
**viewa-ecommerce-integration** skill gives any [Agent Skills](https://agentskills.io)
compatible agent — Claude Code, Codex, Copilot, Cursor, Gemini CLI, and others — a
structured, checkpointed walkthrough of the entire implementation.
## Install [#install]
Installation goes live with the API's general availability, when this site is
published at `docs.viewa.sh`. Until then the command below will not resolve.
```bash
npx skills add https://docs.viewa.sh
```
Or point your agent at the skill directly:
[docs.viewa.sh/skill.md](/skill.md)
## What it does [#what-it-does]
The skill drives your agent through the integration in verified phases:
1. **Preflight** — detects your e-commerce platform (Shopify, WooCommerce, custom
storefront), confirms API keys are in place, and pings your catalog.
2. **Auth wiring** — publishable vs secret key placement, domain allowlist checks.
3. **Catalog wiring** — maps your store's products (and your own SKUs, via aliasing)
to the Viewa catalog.
4. **Path choice** — widget embed or raw API, with a platform-aware recommendation.
5. **Implementation** — the chosen path, end to end.
6. **Verification** — a real render, completed and displayed, on your own instance.
7. **Hardening** — error handling by stable codes, rate-limit backoff, privacy
obligations.
It finishes by writing a `VIEWA_INTEGRATION.md` summary into your repo — a record of
what was integrated where, and the first thing our support team will ask for.
## What to expect [#what-to-expect]
The skill is thin by design: it orchestrates, and defers every API fact to this
documentation site, which your agent reads as it works — so it is always in sync with
the live API.
Your agent will stop and ask **you** at three points: placing API keys (it will never
accept them in conversation — [see why](/docs/ecommerce/getting-started/authentication)),
choosing the integration path, and deciding where visualiser entry points belong in
your store's UI.
# Overview (/docs/ecommerce)
The Viewa E-commerce Visualisation API lets your product pages show customers what a
window treatment looks like **in their own home**: they upload a photo, mark out the
area to cover, pick a product and material, and receive an AI-generated composite of
your product in their space.
Everything in the pipeline is asynchronous — you `POST` to create a resource and poll
its status until it's ready.
## Build with an AI agent [#build-with-an-ai-agent]
These docs are written for coding agents as much as for people, and the fastest path
to a working integration is usually to hand them to one:
* **[/llms.txt](/llms.txt)** — a machine-readable index of every page, and
**[/llms-full.txt](/llms-full.txt)** — the entire documentation in one document.
Point your agent at either to give it the full picture.
* **Every page is available as raw Markdown** — use the **Copy Markdown** button, or
the **Open** menu to send a page straight to Claude or ChatGPT with context attached.
* The API reference is generated from our OpenAPI 3.1 specification, so an agent can
rely on it being exact: paths, schemas, auth, and error codes match the live API.
**[The integration skill](/docs/ecommerce/agent-skill)** walks your coding agent
through the whole implementation — keys, catalog wiring, widget embedding, and
render handling — end to end, with verified checkpoints at every phase.
## Two ways to integrate [#two-ways-to-integrate]
Most integrations start with the widget — it ships the photo-upload and corner-editing
UX so you don't have to build it. The raw API is there when you need complete control
over the experience.
## Where to start [#where-to-start]
# Viewa Surface (/docs/surface)
Viewa Surface is the platform behind everything documented on this site: an AI-powered
visualisation engine that shows customers what window treatments look like in their own space.
## Implementing the visualiser [#implementing-the-visualiser]
The [Implementation Guide](/docs/surface/implementation) walks through integrating the
visualiser into your website — navigation buttons, CTA banners, landing pages, product
page buttons, and pre-selecting products and materials.
## Modules [#modules]
# Allow an origin (/docs/ecommerce/api/addOrigin)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Define areas on an image (/docs/ecommerce/api/createAreaSet)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Upload a photo (/docs/ecommerce/api/createImage)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Request a composite render (/docs/ecommerce/api/createRender)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove a SKU alias (/docs/ecommerce/api/deleteAlias)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete an area set (/docs/ecommerce/api/deleteAreaSet)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Delete a visitor's data (/docs/ecommerce/api/deleteVisitor)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get one SKU alias (/docs/ecommerce/api/getAlias)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Read an area set, or a single area (/docs/ecommerce/api/getArea)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get an image and advance its preprocessing (/docs/ecommerce/api/getImage)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fetch an uploaded photo (/docs/ecommerce/api/getImageFile)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a material (/docs/ecommerce/api/getMaterial)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a material set (/docs/ecommerce/api/getMaterialSet)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a product (/docs/ecommerce/api/getProduct)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Get a render (/docs/ecommerce/api/getRender)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Fetch a succeeded render (/docs/ecommerce/api/getRenderFile)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Per-key usage for a period (/docs/ecommerce/api/getUsage)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# API Reference (/docs/ecommerce/api)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List your registered SKU aliases (/docs/ecommerce/api/listAliases)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List an image's area sets (/docs/ecommerce/api/listAreaSets)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List your key's allowed origins (/docs/ecommerce/api/listOrigins)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List the materials offered in one of a product's slots (/docs/ecommerce/api/listProductSlotMaterials)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List products (/docs/ecommerce/api/listProducts)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List renders for an area set (/docs/ecommerce/api/listRenders)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# List the shopper's own renders (/docs/ecommerce/api/listVisitorRenders)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Register or repoint a SKU alias (/docs/ecommerce/api/putAlias)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Remove an allowed origin (/docs/ecommerce/api/removeOrigin)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Update an area set's corners (/docs/ecommerce/api/updateAreaSet)
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
# Areas (/docs/ecommerce/concepts/areas)
An **area** is one opening in the customer's photo — a window, a door, a garage
entrance. Once a photo is `ready`, you send the four corners of each area and the
API works out how big that opening really is and how it sits in space, so a
product can be placed into it convincingly.
Areas are created, updated and referenced as a **set**. The estimation runs across
every area in the set at once rather than one at a time, so the corners for one
photo belong in a single call.
Send the shopper's `X-Viewa-Visitor-Token` alongside your API key on every call on
this page — an area set belongs to the visitor who created it, and some instances
require the token. See [Visitor tokens](/docs/ecommerce/concepts/visitor-tokens).
```bash
curl -X POST https://your-instance.example/api/v1/areas \
-H "Authorization: Bearer pk_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8c2e41a7-..." \
-d '{
"image": "img_7bQ2mR9vXpL4dK1sZeCt",
"areas": [
{ "corners": [[0.21, 0.30], [0.78, 0.30], [0.78, 0.82], [0.21, 0.82]] }
]
}'
```
```json
{
"id": "aset_9mK3pQ7wZxN2vC5tBhLr",
"image": "img_7bQ2mR9vXpL4dK1sZeCt",
"status": "processing",
"areas": [
{
"id": "area_4nH8xW2qTvB6yJ0aRmDp",
"name": null,
"corners": [[0.21, 0.3], [0.78, 0.3], [0.78, 0.82], [0.21, 0.82]],
"measurements": { "width": null, "height": null, "unit": "m" },
"plane": { "rotation": null, "center": null }
}
],
"error": null
}
```
## Corner points [#corner-points]
Corners are `[x, y]` pairs **normalized to 0..1** against the photo. Normalized
coordinates mean your corner-picking UI doesn't have to care what resolution the
photo is displayed at — or that the API resized it on upload.
**Order doesn't matter.** Four points describe exactly one shape, so the API sorts
them around the perimeter for you. If a customer drags a corner past its neighbour
and the outline crosses itself, that's normalized rather than rejected — your editor
doesn't need to keep them in order. The response returns the sorted order.
Four points, no more, no fewer. These are rejected with `invalid_corners`:
* anything other than exactly four points
* a coordinate outside `0..1`
* two corners at the same point
* a shape so thin or flat it encloses no real area
The `detail` on the error names which area failed and why, so you can put the
message straight in front of the customer.
## Naming an area [#naming-an-area]
Each area takes an optional `name` — free text, up to 120 characters, purely
for your own UI:
```json
{
"image": "img_7bQ2mR9vXpL4dK1sZeCt",
"areas": [
{ "name": "Living room window", "corners": [[0.10, 0.30], [0.44, 0.30], [0.44, 0.80], [0.10, 0.80]] },
{ "name": "Kitchen window", "corners": [[0.56, 0.28], [0.90, 0.28], [0.90, 0.78], [0.56, 0.78]] }
]
}
```
It is a label, never an identifier — two areas may share a name, and it plays
no part in matching areas on update. `id` is the identity. Omit `name` and the
area comes back with `"name": null`.
## Measurements arrive with `ready` [#measurements-arrive-with-ready]
Notice the `201` above comes back with `measurements` and `plane` as `null`. The
size estimate and the occlusion pass run together in one background job, so poll
the set and read them once `status` is `ready`.
`measurements` are in **meters**, estimated from the photo's depth data. `plane`
describes how the opening is oriented in space; you only need it if you're doing
your own placement, and can ignore it otherwise.
## The lifecycle [#the-lifecycle]
`status` covers the remaining preparation, which finishes in the background:
```
POST /api/v1/areas → { "id": "aset_…", "status": "processing", areas: [...] }
GET /api/v1/areas/{id} → poll until "ready" (or "failed")
```
| `status` | Meaning |
| ------------ | ------------------------------------------------------------------------------ |
| `processing` | Measuring and preparing. `measurements` and `plane` are `null` — keep polling. |
| `ready` | Complete. Required before you can request a render. |
| `failed` | Terminal. `error` says why. |
As with images, a `failed` set is **resource state, not an HTTP error** — the poll
still answers `200` with a structured `error`. `error.code` is stable:
`occlusion_failed`, `occlusion_timeout` or `area_calculation_failed`. Recreate
the set to retry. See
[Errors](/docs/ecommerce/concepts/errors#job-failures-are-not-errors).
## Reading one area [#reading-one-area]
`GET /api/v1/areas/{id}` takes either id. Pass the set's `aset_…` id for the whole
set, or a single `area_…` id for just that area:
```
GET /api/v1/areas/aset_9mK3pQ7wZxN2vC5tBhLr → the set
GET /api/v1/areas/area_4nH8xW2qTvB6yJ0aRmDp → one area
```
To find the sets already defined on a photo:
```
GET /api/v1/areas?image=img_7bQ2mR9vXpL4dK1sZeCt
```
That list is paginated and does **not** advance a set's status — poll the single
resource for that.
## Editing a set [#editing-a-set]
`PUT` replaces the set's areas. Everything derived from them is recalculated and
`status` returns to `processing`:
```bash
curl -X PUT https://your-instance.example/api/v1/areas/aset_9mK3pQ7wZxN2vC5tBhLr \
-H "Authorization: Bearer pk_..." \
-H "Origin: https://shop.example" \
-H "Content-Type: application/json" \
-d '{
"areas": [
{ "id": "area_4nH8xW2qTvB6yJ0aRmDp",
"corners": [[0.25, 0.35], [0.85, 0.35], [0.85, 0.85], [0.25, 0.85]] }
]
}'
```
**Send every area you want the set to have** — the `PUT` replaces it. One call
covers every edit:
| To do this | Send |
| -------------- | -------------------------------------- |
| Move a corner | The area's `id` with its new `corners` |
| Rename an area | The area's `id` with a new `name` |
| Add an area | An area with **no** `id` |
| Remove an area | Nothing — just leave it out |
| Reorder areas | The same ids in a different order |
**Identity comes from `id`, never from position.** Send an area's `id` back and
it keeps that id, so any product or material selections you have stored against
it stay valid — wherever it now sits in the array. This is why reordering is
safe: if identity were inferred from position, a UI that re-sorts areas
left-to-right would silently repoint `area_4nH8...` at a different window, and
the customer would get the wrong blind on the wrong opening.
An `id` that isn't part of this set — or the same `id` twice in one request —
is rejected with `unknown_area`.
You can't remove the *last* area this way; a set with no areas has no meaning.
Use `DELETE /api/v1/areas/{id}` to discard the whole set.
If the recalculation fails, the set becomes `failed` with
`area_calculation_failed`; send another `PUT` with adjusted corners, or create a
new set.
## One photo, many sets [#one-photo-many-sets]
A set stays bound to the photo it was created against. Sending a different `image`
on a `PUT` is rejected with `image_immutable`: every measurement in the set was
derived from that specific photo, so repointing it would be meaningless. Create a
new set instead — the photo itself is reused, and one image can carry as many sets
as you like.
That's also how you offer alternative layouts: create a set per layout, keep the
ids, and render from whichever the customer picks.
## Multiple areas [#multiple-areas]
A photo with two windows is one set with two areas:
```json
{
"image": "img_7bQ2mR9vXpL4dK1sZeCt",
"areas": [
{ "corners": [[0.10, 0.30], [0.44, 0.30], [0.44, 0.80], [0.10, 0.80]] },
{ "corners": [[0.56, 0.28], [0.90, 0.28], [0.90, 0.78], [0.56, 0.78]] }
]
}
```
Each gets its own `area_…` id and its own measurements, returned in the order
you sent them. Send them together rather than as two separate sets: the
estimation considers the whole photo at once, and a render covers one set.
## Next [#next]
With a `ready` set, you can [request a render](/docs/ecommerce/concepts/renders) —
the area ids are what you attach product and material selections to.
# Catalog & public IDs (/docs/ecommerce/concepts/catalog)
Your instance's catalog — the products and materials customers can visualise — is
managed with your Viewa contact and read through the
[catalog endpoints](/docs/ecommerce/api).
## Prefixed public IDs [#prefixed-public-ids]
Every object the API exposes has a stable public ID with a type prefix:
| Prefix | Object |
| ------- | -------------- |
| `prod_` | Product |
| `mat_` | Material |
| `set_` | Material set |
| `img_` | Uploaded image |
| `ren_` | Render |
| `vis_` | Visitor |
IDs are opaque — don't parse them beyond the prefix, and don't assume any ordering.
Raw database IDs never appear in the API.
## Products, materials, and material sets [#products-materials-and-material-sets]
A **product** is a configurable window treatment (a roller blind, a drape, a shutter).
Each product exposes one or more **material slots** — the parts of the product a
customer can choose a finish for. The materials valid for a slot are grouped into
**material sets**. See [Slot tokens](/docs/ecommerce/concepts/slot-tokens) for how slots are
addressed.
## Agent-friendly encoding [#agent-friendly-encoding]
`GET /products` and `GET /products/{ref}/materials` can return the same list
envelope as [TOON](https://toon-format.org), a compact tabular encoding that
costs roughly a third to a half fewer tokens than JSON when an AI agent re-reads
the catalog every turn. Ask for it with the `Accept` header:
```bash
curl "$BASE_URL/products" \
-H "Authorization: Bearer $API_KEY" \
-H "Accept: text/toon"
```
The response is `text/toon; charset=utf-8` and decodes to exactly the JSON
`{ data, pagination }` envelope — it is an encoding, not a different schema. JSON
stays the default for any other `Accept` value, single-resource endpoints are
JSON only, and errors are always `application/problem+json`.
## Products with styles and API-only products [#products-with-styles-and-api-only-products]
Some products in the Viewa visualiser publish **styles** — several pre-authored
looks (an S-wave drape, a pinch-pleat drape) selected inside a single product.
That is a visualiser affordance, and those products are not published on the API:
they never appear in `GET /products`, can't be fetched by ID or SKU, can't be
aliased, and are rejected by the render endpoint like any unknown product.
If a product you need is missing for this reason, ask your Viewa contact to model
each look as its own flat product. A product intended only for integrators can be
marked **API only**, which publishes it here while keeping it out of the
visualiser — so your catalog and the visualiser's can differ deliberately.
Because of this, `styles` on a product detail response is always an empty array
and `stylesLabel` is always `null` in v1. `styleOptions` is unrelated and is
populated as normal — it is the product's own configuration vocabulary.
## Child instances [#child-instances]
If your account has child instances (for example, per-brand or per-region stores), a
child reads its parent's catalog narrowed to its configured subset. Products outside
the subset are absent from responses entirely, exactly as if they didn't exist.
## Referring to catalog objects [#referring-to-catalog-objects]
Endpoints that take a catalog reference accept either the Viewa public ID or, if
you've registered them, your own identifiers — see
[SKU aliasing](/docs/ecommerce/concepts/sku-aliasing).
# Errors (/docs/ecommerce/concepts/errors)
Every error the API returns is an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)
`application/problem+json` document with a stable, machine-readable `code`:
```json
{
"type": "https://docs.viewa.sh/errors#example-code",
"title": "Human-readable summary",
"status": 422,
"code": "example_code",
"detail": "What specifically went wrong with this request."
}
```
## Handle the `code`, not the message [#handle-the-code-not-the-message]
`title` and `detail` are for humans and may be reworded at any time. `code` values are
part of the API contract: stable, additive-only, and safe to branch on.
```ts
if (problem.code === 'example_code') {
// handle it
}
```
## Job failures are not errors [#job-failures-are-not-errors]
problem+json is reserved for **request-level** failures: a bad or missing key, an
unknown id, a rejected upload. When an asynchronous *job* fails — a photo's
preprocessing, a render — that is a state the resource is in, not a problem with
the request that asked about it. Polling a failed resource answers `200`, with a
structured `error` object on the body:
```json
{ "id": "img_…", "status": "failed", "error": { "code": "preprocessing_failed", "detail": "…" } }
```
Those nested `error.code` values are just as stable as problem+json codes — see
[Photos & preprocessing](/docs/ecommerce/concepts/images) for the image ones and
[Areas](/docs/ecommerce/concepts/areas) for the area-set ones:
`occlusion_failed`, `occlusion_timeout`, and `area_calculation_failed` — the
size and orientation of one or more areas could not be estimated from the photo;
adjust the corners and try again. Your polling loop only ever branches on
`status`; it never needs an HTTP error handler for a job outcome.
## Error catalog [#error-catalog]
Codes already live on the image endpoints: `unsupported_media_type` (415),
`file_too_large` (413), `not_found` (404), `invalid_request` (400) — and on
the areas endpoints: `invalid_corners` (400), `image_immutable` (400),
`unknown_area` (400), `image_not_ready` (409) — plus the auth codes shared by
every endpoint. Render requests add
`invalid_selections` (400), `area_set_not_ready` (409), and
`idempotency_key_reused` (409). The pipeline endpoints also return
`visitor_token_required` (403) when no `X-Viewa-Visitor-Token` was sent, and
`visitor_token_invalid` (403) when the one sent is malformed, expired, or was
issued for a different instance. The first applies only where the token is
mandatory (an instance configured to require it, and the history endpoint
always); the second applies anywhere a token is sent — see [Visitor
tokens](/docs/ecommerce/concepts/visitor-tokens). The pipeline `POST`s add two
more: `rate_limited` (429) for a transient burst or concurrency refusal, which
carries a `Retry-After` header, and `quota_exceeded` (429) when a daily
allowance is used up, which does not — see
[Limits](/docs/ecommerce/concepts/limits). One code is not about your request
at all: `internal_error` (500) means something went wrong on our side. It is
returned by `DELETE /visitors/{id}` when a deletion does not complete, and the
request is safe to retry.
Render job failures are returned in the resource's `error` object rather than as
problem+json. The closed list is `capture_failed`, `capture_timeout`,
`generation_failed`, `generation_timeout`, `dispatch_failed`, and `internal`.
# Photos & preprocessing (/docs/ecommerce/concepts/images)
A visualisation session starts with the customer's photo. Upload it in a single
request — the file goes in the `POST` body — and you get back an image resource
(`img_…`) that is already being preprocessed:
```bash
curl -X POST https://your-instance.example/api/v1/images \
-H "Authorization: Bearer pk_..." \
-F "file=@room.jpg"
```
```json
{
"id": "img_7bQ2mR9vXpL4dK1sZeCt",
"status": "preprocessing",
"width": 1024,
"height": 768,
"url": "https://your-instance.example/api/v1/images/img_7bQ2mR9vXpL4dK1sZeCt/file",
"error": null,
"visitor": {
"id": "vis_5nQ8xR2wKt7bZm3LpVdC",
"token": "vis_5nQ8xR2wKt7bZm3LpVdC.1788307200.hSg9…",
"expiresAt": "2026-12-01T00:00:00.000Z"
}
}
```
Send the file either as a `multipart/form-data` part named `file` (what a browser
`FormData` produces) or as the raw request body — both work.
This is the only pipeline call that never requires a visitor token, and it is where
tokens come from: the response also carries a `visitor` block whose `token` you
should store and send as `X-Viewa-Visitor-Token` on every later call, this image's
own `GET` included. Whether later calls *require* it is a per-instance setting —
see [Visitor tokens](/docs/ecommerce/concepts/visitor-tokens).
## What the server accepts [#what-the-server-accepts]
JPEG, PNG, or WebP, up to 4 MB. Validation happens server-side against the
**actual bytes**, not your declared `Content-Type` — a mislabelled or undecodable
file is rejected with a [problem+json error](/docs/ecommerce/concepts/errors)
(`unsupported_media_type`, 415) and an oversized one with `file_too_large` (413).
The photo is resized server-side before preprocessing, so the resource's `width`
and `height` are the working dimensions, not those of the file you sent. The stored
copy is normalised: the EXIF orientation is applied to the pixels and the bytes are
re-encoded as JPEG with the camera EXIF block kept, so you never need to pre-rotate
a phone photo.
## The lifecycle [#the-lifecycle]
Preprocessing extracts what the later pipeline stages need from the photo (depth,
surface geometry, segmentation). It takes real time, so the resource is
asynchronous: poll it, and watch `status` move.
```
POST /api/v1/images → { "id": "img_…", "status": "preprocessing" }
GET /api/v1/images/{id} → poll until "ready" (or "failed")
```
| `status` | Meaning |
| --------------- | --------------------------------------------------------------- |
| `preprocessing` | Queued or running — keep polling. |
| `ready` | Usable for [areas](/docs/ecommerce/concepts/areas) and renders. |
| `failed` | Terminal. `error` says why; upload a new photo to retry. |
A `failed` image is **resource state, not an HTTP error**: the poll still answers
`200`, with a structured `error` object on the body —
```json
{ "id": "img_…", "status": "failed", "error": { "code": "preprocessing_failed", "detail": "…" } }
```
`error.code` is stable and safe to branch on: `preprocessing_failed` (the job
errored or could not be queued) or `preprocessing_timeout` (it did not finish in
time). Either way the resource is terminal — retry by uploading again.
problem+json is reserved for request-level failures (bad key, unknown id,
rejected file); see [Errors](/docs/ecommerce/concepts/errors#job-failures-are-not-errors).
## The photo URL [#the-photo-url]
`url` serves the stored photo through the API itself and takes **no API key** —
it works directly as an `
`. The 20-character id is the credential; the
underlying storage host never appears in the contract. The bytes behind an id
never change (a new upload mints a new id), so the response is cacheable
indefinitely. `url` is `null` only if the photo bytes were never stored — an
image that `failed` before storage keeps a null `url`.
## Idempotency [#idempotency]
`POST /v1/images` honours an `Idempotency-Key` header, same as renders: a retried
upload with the same key returns the original image resource instead of
processing the photo twice. Key the *attempt* — generate a UUID when the customer
picks a photo, reuse it for retries of that same pick.
# Limits & uploads (/docs/ecommerce/concepts/limits)
## Upload constraints [#upload-constraints]
| Constraint | Value |
| ---------- | --------------- |
| Formats | JPEG, PNG, WebP |
| Max size | 4 MB |
HEIC (the iPhone default) is **not** accepted — convert before upload. The
[widget](/docs/ecommerce/widget/embed) handles format guidance for the customer automatically.
## The limits [#the-limits]
Every limit below is enforced at job creation: an over-limit request fails fast
and no GPU work is ever queued, so a rejection costs you nothing.
| Limit | Default | Scope |
| -------------------------- | --------- | --------------------------- |
| Uploads per shopper | 10 | Rolling 24 hours |
| Uploads per key | 200 | Rolling 24 hours |
| Renders per shopper | 25 | Rolling 24 hours |
| Renders per key | 200 | Rolling 24 hours |
| Concurrent renders per key | 2 | In flight at once |
| Requests per IP | 60/minute | Across the pipeline `POST`s |
The daily windows are **rolling**, not calendar days: capacity returns
continuously as the oldest events pass the 24-hour mark, rather than all at once
at midnight.
Every value is configurable per instance (set on the instance's API tab by your
Viewa contact), and can be overridden per API key — so a server-side key and a
browser key on the same instance can carry different ceilings. Changes take
effect immediately, with no redeploy on your side. Contact the Viewa team to
have a limit raised.
### Cache hits are free [#cache-hits-are-free]
A render whose selections exactly match one you have already run is served from
the composition-hash cache. It never reaches the GPU, so it consumes no quota and
counts toward no cap — even for a shopper who is already at their daily limit.
It is still reported (as `cachedRenders`) so your usage numbers reconcile.
### Without a visitor token [#without-a-visitor-token]
The per-shopper caps need a shopper to attribute the request to. If your
instance does not [require visitor tokens](/docs/ecommerce/concepts/visitor-tokens)
and your client sends none, every upload mints a new visitor, so the per-shopper
upload cap has nothing to bite on: those callers are held to the per-key daily
cap and the per-IP burst limit instead. Renders are unaffected — a render
inherits its image's shopper either way.
## When you hit one [#when-you-hit-one]
Both refusals are `429` with problem+json — see
[Errors](/docs/ecommerce/concepts/errors).
| `code` | What it means | What to do |
| ---------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `rate_limited` | Transient: the per-IP burst, or your key's concurrent-render ceiling. | Wait the seconds in the `Retry-After` header, then retry. |
| `quota_exceeded` | A daily allowance is used up. `detail` names which cap and its value. | Don't spin: capacity returns as the rolling window advances. Surface it to the shopper, or ask Viewa to raise the cap. |
Only `rate_limited` carries `Retry-After`. Treat a `quota_exceeded` as a state
to report, not a call to retry in a loop.
## Checking your usage [#checking-your-usage]
`GET /api/v1/usage` reports what each of your keys consumed, for reconciling an
invoice. It is **secret-key only** — it enumerates every key on your instance,
which a browser never needs.
```bash
curl "https://your-instance.example/api/v1/usage?from=2026-09-01T00:00:00Z" \
-H "Authorization: Bearer sk_..."
```
```json
{
"period": { "from": "2026-09-01T00:00:00.000Z", "to": "2026-09-14T09:12:44.301Z" },
"data": [
{
"key": { "label": "Storefront", "publishableKey": "pk_...", "status": "active" },
"uploads": 412,
"renders": 287,
"cachedRenders": 96
}
]
}
```
`from` and `to` are RFC 3339 date-times **with an offset** (`...Z` or
`...+10:00`) — a bare date is rejected rather than guessed at — and the period is
half-open, `[from, to)`, so consecutive months never double-count an event. Omit
them and you get the current UTC calendar month up to now. A period may span at
most 400 days.
`renders` counts render jobs dispatched to the pipeline (cache hits excluded,
listed under `cachedRenders`); a render that failed to dispatch is neither
billed nor counted. `cachedRenders` is shown alongside it so the totals
reconcile.
Rate limiting uses Upstash Redis. Leave the `UPSTASH_REDIS_REST_*` variables
unset in local development to disable IP burst limiting, or point them at a
dedicated dev database — never the production one.
# Renders & idempotency (/docs/ecommerce/concepts/renders)
Rendering a composite takes real time — it's an AI image-generation step, not a
template fill. The API models this explicitly.
Send the shopper's `X-Viewa-Visitor-Token` alongside your API key when creating,
polling and listing renders — a render belongs to the visitor who requested it, and
some instances require the token. See [Visitor
tokens](/docs/ecommerce/concepts/visitor-tokens).
## Asynchronous by design [#asynchronous-by-design]
`POST` a render request and you get back a render resource (`ren_…`) immediately, with
a status. Poll it until it completes:
```
POST /api/v1/renders → { "id": "ren_…", "status": "…" }
GET /api/v1/renders/{id} → poll until complete
```
Polling is the v1 contract: it is how a client learns the outcome. The render
completes on its own whether or not anyone polls it, and a render past its time
budget is marked `failed` by a read or by the scheduled reaper. A terminal
`succeeded` or `failed` resource is returned with HTTP `200`.
Poll on a schedule shaped by when a render can finish rather than at a flat
rate: no request in the first 3 s — none completes sooner — then every 500 ms
out to 12 s, every second to 30 s, and every 2.5 s after that, until the render's
ten minute budget is spent. Honour a `Retry-After` header if one comes back. The
[widget](/docs/ecommerce/widget/embed) implements exactly this.
The status values are:
| Status | Meaning |
| ------------ | -------------------------------------------------- |
| `queued` | The render was created and is waiting for capture. |
| `capturing` | The product capture is in progress. |
| `generating` | The realism generation job is running. |
| `succeeded` | The immutable composite is available at `url`. |
| `failed` | Terminal; inspect `error.code` and `error.detail`. |
## Request selections [#request-selections]
The area set must be `ready`. A render may select a subset of its areas; omitted
areas render empty. Each selected area needs a product, while style, style options,
materials and animation are optional and default from the selected model/product.
The field names match the [catalog DTO](/docs/ecommerce/concepts/catalog):
```bash
curl -X POST https://your-instance.example/api/v1/renders \
-H "Authorization: Bearer pk_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f9d36c2-..." \
-d '{
"areaSet": "aset_9mK3pQ7wZxN2vC5tBhLr",
"areas": [{
"area": "area_4nH8xW2qTvB6yJ0aRmDp",
"product": "prod_3xK9aQ0ZbW7mNc2LpRvT",
"style": "s-wave",
"styleOptions": [{ "type": "rollDirection", "value": "standard" }],
"materials": [{ "optionId": "slot_66c1f2a9e4b1c2d3f4a5b6c7", "material": "mat_7Bq2LmZ0xR4vNc9TgWpK" }]
}]
}'
```
Products and materials may also be supplied as registered SKUs. Style options
are validated against the selected style's own model schema. Animation values are
numbers from `0` to `1`; omit the array to use model defaults.
## Idempotency [#idempotency]
Send an `Idempotency-Key` header with every render `POST`. If the request is retried —
a flaky connection, a user double-click — the API returns the original render instead
of creating a duplicate:
```http
POST /api/v1/renders
Idempotency-Key: 4f9d36c2-...
```
Use a key that identifies the *attempt* (generate a UUID when the customer clicks
"visualise", and reuse it for retries of that same click). If the same key is
reused with different canonical selections, the API returns `409
idempotency_key_reused`.
A successful composition cache hit is different: it creates a new succeeded
resource that shares the existing immutable artifact.
## Result URL and durable snapshots [#result-url-and-durable-snapshots]
On success, `url` points to `/api/v1/renders/{id}/file`. The file endpoint needs no
API key, allows wildcard CORS, and supports `width` (maximum 1024 pixels on the
longest edge), `format` (`jpeg`, `webp`, or `png`) and `quality` (`1`–`100`). It
uses an immutable cache policy, so it is suitable for an image element.
The render keeps a geometry and selection snapshot. Editing or deleting the area
set does not alter an existing render, and renders remain listable by `areaSet`.
Catalog content edits do not invalidate a cached artifact or change its URL; a
catalog-aware purge is a later API concern.
Failure is resource state rather than an HTTP error. The closed render error codes
are `capture_failed`, `capture_timeout`, `generation_failed`,
`generation_timeout`, `dispatch_failed`, and `internal`.
# SKU aliasing (/docs/ecommerce/concepts/sku-aliasing)
Your storefront already has SKUs. Rather than maintaining a mapping table on your
side, you can register your identifiers as **aliases** against Viewa catalog objects,
then use them anywhere the API takes a catalog reference.
## How it works [#how-it-works]
An alias binds one of your external IDs to exactly one catalog target (a product, a
material or a material set). Once registered, endpoints that accept a catalog `ref`
resolve your ID exactly as if you'd passed the Viewa public ID. A product alias can also
pin a material, for a SKU that identifies a product and fabric/colour together.
Aliases are scoped to your instance — your SKUs never collide with another client's.
SKUs match case-insensitively and ignore surrounding whitespace.
## Managing aliases [#managing-aliases]
Manage aliases from your server with your secret key. The SKU itself is the address,
so there is no separate alias id to track:
| Call | Does |
| ----------------------------------------------------------------- | ---------------------------------------------------- |
| [`PUT /api/v1/aliases/{sku}`](/docs/ecommerce/api/putAlias) | Register the SKU, or repoint it if it already exists |
| [`GET /api/v1/aliases/{sku}`](/docs/ecommerce/api/getAlias) | Read one registration |
| [`DELETE /api/v1/aliases/{sku}`](/docs/ecommerce/api/deleteAlias) | Remove it |
| [`GET /api/v1/aliases`](/docs/ecommerce/api/listAliases) | Audit every registration |
```http
PUT /api/v1/aliases/ELITE-ROLLER-01
Authorization: Bearer sk_…
Content-Type: application/json
{ "target": "prod_3xK9aQ0ZbW7mNc2LpRvT", "material": "mat_8hT3kW1qZr6YbN0cVxLp" }
```
`PUT` is idempotent, so syncing your catalog is a matter of replaying it for every SKU.
Management writes are limited to 600 a minute per key; on a `429`, wait for the
`Retry-After` seconds and continue. That limit is counted separately, so a sync never
slows your shoppers' uploads or renders.
Changes take effect on the next request. A registration is refused with `422` when its
target isn't in your catalog, when the material isn't offered on the product, or when
the SKU starts with a reserved prefix such as `prod_` or `sk_`.
# Material slots (/docs/ecommerce/concepts/slot-tokens)
A product may have more than one configurable surface — a roller blind has its fabric,
and may also have a pelmet or bottom rail finish. Each of these is a **material slot**.
## Slot tokens [#slot-tokens]
Every slot on a product has an opaque token with the `slot_` prefix. The catalog
endpoints return each product's slots along with the material sets valid for them; when
you make a selection, you address the slot by its token.
Slot tokens are stable for the life of the product configuration, but opaque — don't
construct them, and don't assume meaning in their contents. Read them from the
[product's slot listing](/docs/ecommerce/api) and pass them back.
## Selections [#selections]
A complete selection maps each slot you want to override to a material (by Viewa
public ID or your [aliased SKU](/docs/ecommerce/concepts/sku-aliasing)). Slots you leave out
keep the product's default finish.
# Visitor tokens & privacy (/docs/ecommerce/concepts/visitor-tokens)
Customers using the visualiser on your site are **visitors**. A visitor's uploads,
areas and renders belong to them, and the API scopes every read and write to them
with a signed visitor token.
Your API key says *which shop* is calling. The visitor token says *which shopper*.
**Whether the token is mandatory is a per-instance setting** — off by default,
set on the instance's API tab by your Viewa contact on request. With it off, an
API key alone can read any resource in your instance and protection is the
unguessable `img_`/`aset_`/`ren_` id, exactly as for the `/file` routes. With it
on, every image, area and render call must present the token.
A token you *do* send always scopes the request to that shopper, either way —
so the integration below is the recommended path regardless of the setting, and
nothing changes when the gate is switched on.
## Getting a token [#getting-a-token]
Tokens are issued by the first upload. `POST /api/v1/images` sent **without** a
visitor token mints a new visitor and returns it alongside the image:
```json
{
"id": "img_7bQ2mR9vXpL4dK1sZeCt",
"status": "preprocessing",
"visitor": {
"id": "vis_5nQ8xR2wKt7bZm3LpVdC",
"token": "vis_5nQ8xR2wKt7bZm3LpVdC.1788307200.hSg9…",
"expiresAt": "2026-12-01T00:00:00.000Z"
}
}
```
Persist `visitor.token` wherever you keep shopper state — session storage, a
first-party cookie, your own session record. It is the only way back to that
shopper's data.
## Sending it [#sending-it]
Every later pipeline call carries the token in its own header, next to the API key:
```http
Authorization: Bearer pk_...
X-Viewa-Visitor-Token: vis_5nQ8xR2wKt7bZm3LpVdC.1788307200.hSg9…
```
That applies to reading an image, creating and reading areas, requesting and
polling renders, and listing history. The two unauthenticated `/file` routes are
the exception — an `
` cannot send headers, so the opaque `img_…` /
`ren_…` id is the credential there, exactly as before.
The header is on the CORS preflight allowlist, so browser-direct calls with a
publishable key work unchanged.
## Refresh and expiry [#refresh-and-expiry]
Send the token on later uploads too. The upload then attaches to the **same**
visitor rather than starting a new one, and the response carries the `visitor`
block again with a later `expiresAt`. The expiry slides forward on every upload,
so an active shopper never ages out mid-session — always store the newest token
you were given.
A token's lifetime matches the instance's retention window (90 days by default,
set on the instance's API tab by your Viewa contact). Once it passes, the token
stops verifying and the shopper starts over with a fresh upload.
## When a token is refused [#when-a-token-is-refused]
Both failures are `403` with a stable [`code`](/docs/ecommerce/concepts/errors).
Only the first depends on the instance policy — `visitor_token_required` can
happen only where the token is mandatory (an instance configured to require it,
and the history endpoint, which always is). `visitor_token_invalid` applies
everywhere, because a token that is sent is always verified:
| Code | Meaning | What to do |
| ------------------------ | ------------------------------------------------------ | ------------------------------------------------------------------- |
| `visitor_token_required` | The route needs a visitor token and none was sent | Upload a photo to be issued one |
| `visitor_token_invalid` | Malformed, expired, or issued for a different instance | Discard the stored token and upload again — a new visitor is minted |
Treat `visitor_token_invalid` as "this shopper starts over": drop the token, start
a fresh upload, and store the new one. Do not retry the same token.
A **secret key does not bypass this.** `sk_` satisfies the API-key tier only; a
server-side call still needs the shopper's token to read the shopper's data.
## One shopper never sees another's [#one-shopper-never-sees-anothers]
When a token is presented, a resource that exists but belongs to a different
visitor answers the uniform `404 not_found` — the same response an unknown id
gets. Nothing distinguishes the two, so the API never confirms that someone
else's id exists.
Areas and renders created *without* a token still inherit the shopper their
source image belongs to, so a visitor's documents stay linked whether or not the
client sent the header — which is what lets the deletion below treat them as one
set.
Tokens are instance-bound too: a token issued under one instance is rejected under
another, even for the same `vis_…` id.
## History [#history]
With a valid token you can list the shopper's own renders to power a "your saved
looks" experience:
```bash
curl https://your-instance.example/api/v1/visitors/me/renders \
-H "Authorization: Bearer pk_..." \
-H "X-Viewa-Visitor-Token: vis_..."
```
`me` is whoever the token names; there is no other addressable visitor. This
endpoint requires the token on **every** instance, whatever the setting above —
"the current visitor's renders" has no meaning without a visitor. The
response is the standard paginated list envelope, newest first, and includes every
state — generating, succeeded and failed. Each row is a full render resource, so it
already carries the `image` and `areaSet` it came from, its result `url`, and the
`areas` selections that produced it.
## Deletion [#deletion]
Deleting a visitor is a single secret-key call. Use it to service a shopper's
privacy request:
```bash
curl -X DELETE https://your-instance.example/api/v1/visitors/vis_5nQ8xR2wKt7bZm3LpVdC \
-H "Authorization: Bearer sk_..."
```
```json
{
"visitor": "vis_5nQ8xR2wKt7bZm3LpVdC",
"deleted": { "images": 2, "areaSets": 1, "renders": 2, "objects": 3 },
"usageRecordsDetached": 3
}
```
It takes the visitor's **id**, not their token — your server is acting on the
shopper's behalf, and a shopper exercising deletion has usually lost the token
already. There is no visitor-token requirement on this route, and a publishable
key is refused with `key_type_mismatch`.
What it removes: the visitor's uploaded photos, their areas, their renders, and
the stored files behind all of them. Afterwards their history is empty and their
result URLs stop resolving. `objects` counts the stored files, and is lower than
`images + renders` whenever a render was served from the cache — a cached render
shares the stored file of the one it was deduplicated from. Candidate keys
derived from the deleted records are removed as well; if stored files cannot be
removed after the records are deleted, the response is a `500` and the affected
keys are written to the server log.
What it keeps: your metered usage counts, with the visitor cleared off them.
Those rows are what your invoice is reconciled against, so they survive the
deletion without referencing the shopper.
It is **idempotent**. Calling it again — or for a visitor id your instance never
saw — answers `200` with every count zero, never a `404`. Retry it freely.
### Two residuals to know about [#two-residuals-to-know-about]
**Derived files on the rendering host.** Preprocessing produces depth, normal,
segmentation and mask data on the machine that generates your composites. Those
files are outside this endpoint and age out on that host's own schedule, within
90 days of being written.
**The token is not revoked.** It is stateless — there is no revocation list, so
it keeps verifying until its own expiry. Discard it in your storefront when you
delete the visitor. If you do not, an upload that still presents it will simply
create new data under the same `vis_` id.
**Still to come.** The scheduled retention purge — which will age this same
data out automatically at the end of the instance's retention window — has not
shipped yet. Until it does, deletion is on request only: a token passing its
expiry stops the credential working, it does not remove anything.
# Authentication (/docs/ecommerce/getting-started/authentication)
Viewa uses two API-key tiers, in the same pattern as Stripe. A route's key
requirement is a minimum tier: a secret key satisfies a publishable-key requirement,
but a publishable key cannot satisfy a secret-key requirement.
| Key | Prefix | Where it lives | What it can do |
| --------------- | ------ | ---------------------- | -------------------------------------------------------------------------------- |
| Publishable key | `pk_` | The customer's browser | Access browser-safe routes when the request's `Origin` is on the key's allowlist |
| Secret key | `sk_` | Your server only | Satisfy any API-key tier, including publishable- and secret-key routes |
Both are sent as a bearer token; substitute the key appropriate to the caller:
```http
Authorization: Bearer
```
## Publishable keys are domain-locked [#publishable-keys-are-domain-locked]
A publishable key is the browser-safe minimum tier. It is safe to ship in your
storefront's JavaScript because it is not a secret and can access only the
browser-safe subset of routes. Browser requests are accepted only when their
`Origin` is on the key's **allowlist**. If you see authentication failures from the
browser, check that the page's origin is on the allowlist.
Manage the allowlist from your server with the matching secret key:
[list](/docs/ecommerce/api/listOrigins), [add](/docs/ecommerce/api/addOrigin) and
[remove](/docs/ecommerce/api/removeOrigin) origins at `/api/v1/origins`. Each key pair
has its own list, and a publishable key can never change it.
## Secret keys [#secret-keys]
Your secret key satisfies either API-key tier (`pk` or `sk`). Use it only from your
server: it does not require an `Origin` header, and any `Origin` forwarded by a proxy
is ignored for secret-key authentication. Never put an `sk_` key in browser or other
client-side code. Some routes are secret-key only: `GET /api/v1/usage` — the
per-key [usage report](/docs/ecommerce/concepts/limits) — SKU
[alias management](/docs/ecommerce/concepts/sku-aliasing), the origin allowlist,
and `DELETE /api/v1/visitors/{id}`, which [deletes one shopper's
data](/docs/ecommerce/concepts/visitor-tokens#deletion) on request. Treat it like a password: store it in server-side environment
variables, not source control.
A secret key is not a master bypass. It satisfies the API-key tier only. Visitor
tokens, instance scoping, quotas and metering, and any route-specific authorization
requirements still apply.
If a secret key is exposed, contact the Viewa team to rotate it.
## Visitor tokens [#visitor-tokens]
An API key identifies your shop, not your customer. The pipeline endpoints —
images, areas, renders, and history — also understand a **visitor token**, sent
in its own header:
```http
Authorization: Bearer pk_...
X-Viewa-Visitor-Token: vis_...
```
The token is issued by the first `POST /api/v1/images` and is what keeps one
shopper from reading another's uploads and renders. A secret key does not
substitute for it.
Whether the token is *mandatory* on those endpoints is a per-instance setting,
off by default; the history endpoint always requires it. Send it either way — a
token you send always scopes the call. See [Visitor tokens &
privacy](/docs/ecommerce/concepts/visitor-tokens) for issuance, refresh, expiry,
the instance policy, and the two `403` codes to handle.
## Getting keys [#getting-keys]
Keys are issued per instance by the Viewa team during onboarding — contact your Viewa
representative. Self-service key management is planned.
# Introduction (/docs/ecommerce/getting-started)
## What you'll need [#what-youll-need]
* **API keys** for your instance — see [Authentication](/docs/ecommerce/getting-started/authentication).
Keys are issued by the Viewa team during onboarding.
* A product catalog configured for your instance. Your Viewa contact sets this up with
you; the API then exposes it through the [catalog endpoints](/docs/ecommerce/concepts/catalog).
## The pipeline at a glance [#the-pipeline-at-a-glance]
Every visualisation follows the same shape, whether you use the widget or the raw API:
1. **Upload** — the customer's photo of their space (JPEG, PNG, or WebP).
2. **Areas** — the customer marks the corners of each area to cover (a window, a door
opening). This produces the measurements the renderer needs.
3. **Selection** — your product page tells us which product and material the customer
has chosen, by your own SKU or by Viewa catalog ID.
4. **Render** — request a composite; poll until it's ready; display it.
## Environments [#environments]
All requests go to your instance's API under `/api/v1`. There is no separate sandbox
host — test keys against your own instance during integration.
The API is currently in pilot. Surfaces marked as **in progress** in these docs are
being finalised and may change before general availability.
# Quickstart (/docs/ecommerce/getting-started/quickstart)
The steps below sketch the raw-API path. If you're embedding the
[widget](/docs/ecommerce/widget/embed), it performs steps 2–4 for you.
The raw `curl` examples below represent calls from your server, so they use a secret
key and send no `Origin` header. Keep the `sk_` key in a server-side environment
variable; never copy it into browser or client-side code. For a browser-direct call,
use a publishable key instead. The browser's actual `Origin` must be on that key's
allowlist — do not fabricate an `Origin` header from a server.
The secret key satisfies the API-key tier only. It does not replace visitor tokens,
instance scoping, quotas or metering, or any route-specific authorization.
## 1. Browse your catalog [#1-browse-your-catalog]
List the products available to your instance:
```bash
curl https://your-instance.example/api/v1/products \
-H "Authorization: Bearer sk_..."
```
Every catalog object has a stable, prefixed public ID (`prod_…`, `mat_…`, `set_…`) —
see [Catalog & public IDs](/docs/ecommerce/concepts/catalog). If you've registered
[SKU aliases](/docs/ecommerce/concepts/sku-aliasing), you can refer to products by your own SKUs
instead.
Calling from the browser with a publishable key is supported: every `/v1` endpoint answers a
CORS preflight. The key's domain allowlist is enforced on the request itself, so add your
storefront's origin to the key before you call it from the page — with
[`POST /api/v1/origins`](/docs/ecommerce/api/addOrigin) and your secret key.
## 2. Upload a photo [#2-upload-a-photo]
Upload the customer's photo (JPEG, PNG, or WebP, up to 4 MB) to start a
visualisation session. The file goes in the request body — one call, no separate
upload target:
```bash
curl -X POST https://your-instance.example/api/v1/images \
-H "Authorization: Bearer sk_..." \
-H "Idempotency-Key: 4f9d36c2-..." \
-F "file=@room.jpg"
```
The response is an image resource (`img_…`) already in `preprocessing` — plus the
**visitor** this shopper now is:
```json
{
"id": "img_7bQ2mR9vXpL4dK1sZeCt",
"status": "preprocessing",
"visitor": {
"id": "vis_5nQ8xR2wKt7bZm3LpVdC",
"token": "vis_5nQ8xR2wKt7bZm3LpVdC.1788307200.hSg9…",
"expiresAt": "2026-12-01T00:00:00.000Z"
}
}
```
Store `visitor.token`. Every later call in this quickstart sends it as
`X-Viewa-Visitor-Token` — your API key identifies the shop, the visitor token
identifies the shopper, and a token you send always scopes the call to that
shopper. Sending it is optional unless your instance is configured to require it
(off by default), so build it in now and nothing changes if that gate is later
switched on. See [Visitor tokens](/docs/ecommerce/concepts/visitor-tokens).
Poll the image until its status is `ready`:
```bash
curl https://your-instance.example/api/v1/images/img_7bQ2mR9vXpL4dK1sZeCt \
-H "Authorization: Bearer sk_..." \
-H "X-Viewa-Visitor-Token: vis_..."
```
See [Photos & preprocessing](/docs/ecommerce/concepts/images) for the full
lifecycle, including how a failed preprocess surfaces.
## 3. Define areas [#3-define-areas]
Submit the corner points the customer marked for each opening to cover. Corners are
`[x, y]` pairs normalised to `0..1` against the photo, ordered around the perimeter.
Send every area for the photo in one call — they are prepared as a set:
```bash
curl -X POST https://your-instance.example/api/v1/areas \
-H "Authorization: Bearer sk_..." \
-H "X-Viewa-Visitor-Token: vis_..." \
-H "Content-Type: application/json" \
-d '{
"image": "img_7bQ2mR9vXpL4dK1sZeCt",
"areas": [
{ "corners": [[0.21, 0.30], [0.78, 0.30], [0.78, 0.82], [0.21, 0.82]] }
]
}'
```
The response is an area set (`aset_…`) in `processing`, with each area's
`measurements` and `plane` still `null` — they are estimated in the background.
Poll until the set is `ready`: that is when the estimated real-world
`measurements` appear, and a render requires it.
```bash
curl https://your-instance.example/api/v1/areas/aset_9mK3pQ7wZxN2vC5tBhLr \
-H "Authorization: Bearer sk_..." \
-H "X-Viewa-Visitor-Token: vis_..."
```
Keep the `area_…` ids — they are what you attach product and material selections
to in the next step. See [Areas](/docs/ecommerce/concepts/areas) for corner
validation, multi-area photos, and what happens when a customer drags a corner.
## 4. Request a render [#4-request-a-render]
`POST` a render with the selected product and material. The area set must be
`ready`; use the `area_…` ids returned by the area-set response. Renders are
asynchronous: the response is a render resource whose status you poll.
```bash
curl -X POST https://your-instance.example/api/v1/renders \
-H "Authorization: Bearer sk_..." \
-H "X-Viewa-Visitor-Token: vis_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4f9d36c2-..." \
-d '{
"areaSet": "aset_9mK3pQ7wZxN2vC5tBhLr",
"areas": [{
"area": "area_4nH8xW2qTvB6yJ0aRmDp",
"product": "prod_3xK9aQ0ZbW7mNc2LpRvT",
"materials": [{ "optionId": "slot_66c1f2a9e4b1c2d3f4a5b6c7", "material": "mat_7Bq2LmZ0xR4vNc9TgWpK" }]
}]
}'
```
```bash
curl https://your-instance.example/api/v1/renders/ren_2pL7vQ4mXa9cN6tBhRzK \
-H "Authorization: Bearer sk_..." \
-H "X-Viewa-Visitor-Token: vis_..."
```
Use an [`Idempotency-Key`](/docs/ecommerce/concepts/renders) on the `POST` so a retried request
never produces (or bills) a duplicate render.
## 5. Display the result [#5-display-the-result]
Poll the render until `status` is `succeeded` or `failed`:
```bash
curl https://your-instance.example/api/v1/renders/ren_2pL7vQ4mXa9cN6tBhRzK \
-H "Authorization: Bearer sk_..." \
-H "X-Viewa-Visitor-Token: vis_..."
```
When it succeeds, the resource's `url` is a durable, unauthenticated image URL
for display on your product page. A failed render remains a `200` resource with
an `error.code`; retry the request with a new idempotency key when appropriate.
## 6. Show their history [#6-show-their-history]
The same token lists everything this shopper has rendered on your instance, newest
first — enough to build a "your saved looks" strip on a return visit:
```bash
curl https://your-instance.example/api/v1/visitors/me/renders \
-H "Authorization: Bearer sk_..." \
-H "X-Viewa-Visitor-Token: vis_..."
```
Each row is a full render resource, carrying its `image`, its `areaSet`, its result
`url`, and the `areas` selections that produced it. A shopper only ever sees their
own — see [Visitor tokens](/docs/ecommerce/concepts/visitor-tokens).
# Embedding the widget (/docs/ecommerce/widget/embed)
The widget is a **framework-agnostic web component**: one script tag and one element, no
build-tool integration required. It handles the photo upload, the corner editor for
marking areas, and displaying the finished render — your product customizer drives it
through the [JS API](/docs/ecommerce/widget/js-api).
## The shape of it [#the-shape-of-it]
```html
```
Load the script from your API base URL — the same host you call the API on. That is a
complete embed. The widget authenticates with your publishable key — safe in
the browser because keys are
[domain-locked](/docs/ecommerce/getting-started/authentication) — and walks the customer
through the whole journey on its own.
**If you are serving `viewa-widget.js` yourself** — from your own CDN or asset
pipeline rather than from us — add `base-url`. It defaults to the origin the *script*
came from, which in the embed above is the API host; served from your own domain, that
default points every call at your storefront. Your origin also has to be on the key's
allowlist, or every call answers `403 origin_not_allowed`.
Add a product and it visualizes that product with no JavaScript at all:
```html
```
## Attributes [#attributes]
| Attribute | What it does |
| ----------------- | ---------------------------------------------------------------------------------------------------------------- |
| `publishable-key` | **Required.** A `pk_` key whose allowlist includes this origin |
| `base-url` | The API host. Defaults to the origin the script itself came from |
| `capabilities` | Which pipeline steps this instance may take. Defaults to all three |
| `product` | The product to render: a `prod_…` id or a registered SKU alias |
| `materials` | `slot:material` pairs, comma or space separated. Omitted slots use the product's default |
| `product-name` | Overrides the name in the upload prompt. Optional — the widget looks it up |
| `units` | `m` (default), `cm` or `in`. Display only; the API always measures in meters |
| `auto-render` | `on` (default) or `off`. Off means you call `visualize()` |
| `max-areas` | How many areas the customer may mark, 1–20. `1` is single-area mode |
| `ui` | Which controls the widget draws for itself. See [Theming](/docs/ecommerce/widget/theming#choosing-what-is-drawn) |
| `logo` | The mark on the upload panel: a URL for your own, or `none`. Defaults to the Viewa lockup |
| `image` | Resume from a photo you already have (`img_…`) |
| `area-set` | Resume from areas already measured (`aset_…`) |
Only `publishable-key` and `base-url` reset the widget when they change. Everything else
is presentation, so a customizer can rewrite `product` on every product change without
throwing away the photo the customer just uploaded.
`materials` is the markup half of `setSelection`, for a page that has one product and one
finish rather than a picker:
```html
```
Both halves take refs, so a registered SKU alias works in either position. The slot is
required: a material on its own does not say which part of the product it dresses.
## Splitting the journey across pages [#splitting-the-journey-across-pages]
`capabilities` is a subset of `upload`, `areas` and `render`. It is how a storefront
spreads the flow over more than one page — and what a plugin's settings screen maps
checkboxes onto.
```html
```
The set is contiguous, and it also says where to **begin**. A run that does not start at
`upload` cannot obtain its own photo, so it has to be handed one:
| Hand it | And the widget resumes at |
| ---------- | ----------------------------------- |
| `image` | The corner editor, on that photo |
| `area-set` | The measured areas, ready to render |
An area set carries the id of the image it was measured on, so resuming a fully marked-up
scene needs **one** attribute, not two. Setting both only makes the widget resume twice.
Where those ids come from is your session, cart line or order record. Read them off
`getStatus()` when the customer finishes and store them against the line.
A widget whose capabilities start after `upload` with no `image` or `area-set` has
nothing to show, and says so. That is a template mistake rather than a customer-facing
one, so it also logs to the console instead of leaving you guessing.
## What the widget owns vs what you own [#what-the-widget-owns-vs-what-you-own]
| The widget handles | Your page handles |
| -------------------------------- | ------------------------------------------- |
| Photo upload and validation | Product and material choice UI |
| Corner editing and measurement | Passing the selection via `setSelection(…)` |
| Render request, polling, display | Where the widget sits in your layout |
| Visitor token storage | Persisting ids against the cart line |
The widget **never reads your catalog**. Product choice is your customizer's job, which is
why there is no picker inside it — see [Catalog](/docs/ecommerce/concepts/catalog) for
building one.
## Where to put it [#where-to-put-it]
The widget fills the width of its container and holds one height across every step of the
journey, so it drops into a product gallery slot without the page shifting as the customer
works through it. Set `--viewa-aspect` to match your slot.
## Next [#next]
* [JS API](/docs/ecommerce/widget/js-api) — pushing selections in, reading state back out
* [Theming](/docs/ecommerce/widget/theming) — colors, shape, and which controls it draws
# JS API (/docs/ecommerce/widget/js-api)
Your product page already knows what the customer has selected — the JS API is how you
tell the widget. Everything else is optional: a page that only ever sets one product can
do it in markup and never touch JavaScript at all.
## Reaching the element [#reaching-the-element]
With `async` on the script tag the element upgrades whenever the script lands. Attributes
are safe to set before that; **methods are not**, so wait for the definition:
```js
await customElements.whenDefined('viewa-visualizer')
const widget = document.querySelector('viewa-visualizer')
```
## Pushing a selection [#pushing-a-selection]
When the customer changes product or material in your customizer, push it in:
```js
widget.setSelection({ product: 'YOUR-SKU-123' })
```
`product` takes either a minted `prod_…` id or one of your registered
[SKU aliases](/docs/ecommerce/concepts/sku-aliasing). The API resolves both through this
one field, so there is no separate `sku` to set.
Materials are per slot, keyed by [slot token](/docs/ecommerce/concepts/slot-tokens):
```js
widget.setSelection({
product: 'YOUR-SKU-123',
materials: [{ optionId: 'slot_fabric', material: 'CHARCOAL-LINEN' }],
})
```
**`setSelection` merges.** Only the keys you pass are written, so a finish picker can
send `materials` alone without knowing the product ref. Passing a key explicitly as
`undefined` clears it.
A selection triggers a render straight away. Pass `{ render: false }` to batch several
coupled changes, or set `auto-render="off"` to own the trigger entirely and call
`visualize()` yourself.
## Reading state [#reading-state]
`getStatus()` returns everything the widget knows as one snapshot. Reach for it first —
the events carry transitions, and a listener attached late has missed them.
```js
{
phase: 'done', // idle | uploading | preparing | editing |
// measuring | ready | rendering | done | error
step: 'render', // upload | areas | render — the coarse step
busy: false, // waiting on the API — for your spinner
capabilities: ['upload', 'areas', 'render'],
ui: ['chrome', 'controls', 'zoom', 'measurements', 'compare', 'download', 'change-photo'],
productName: 'Vibe Roller Blind',
autoRender: true,
error: null, // { message, code } — customer-safe copy only
image: { id: 'img_…', status: 'ready', url: '…', width: 1024, height: 768 },
areaSet: { id: 'aset_…', status: 'ready' },
areas: [{ id: 'area_…', name: null, measurements: { width, height, unit } }],
activeArea: null, // which area is being edited, in the editor only
measurements: true, // whether the sizes are drawn on the photo
render: { id: 'ren_…', status: 'succeeded', url: '…' },
selection: { product: 'prod_…', materials: [...], animation: [1] },
can: { upload, addArea, removeArea, saveAreas, editAreas, visualize },
}
```
The ids are the part worth keeping. Persist `image` and `areaSet` against a cart line and
a later page can pick the customer up where they left off — see
[Embedding](/docs/ecommerce/widget/embed#splitting-the-journey-across-pages).
`can` is the widget's own answer to "would this do anything right now?". Sync your chrome
off it rather than re-deriving the rule from the phase and getting an edge wrong.
Everything it hands back is a copy, all the way down. Mutating the object you get from
`getStatus()` or `getSelection()` changes nothing inside the widget.
## Methods [#methods]
| Method | What it does |
| ------------------------------------- | -------------------------------------------------------------- |
| `getStatus()` | Everything the widget knows, as one snapshot |
| `setSelection(selection, { render })` | Push the product and material choice. Merges |
| `getSelection()` | What it currently holds |
| `setAnimation(values)` | The open/closed position, 0..1 per animation |
| `visualize()` | Render now. What `auto-render="off"` hands the trigger to |
| `canVisualize` | Whether there is enough to render — areas, product, capability |
| `getAreas()` | The measured areas, once they are ready |
| `chooseFile()` | Open the file picker from your own button |
| `addArea()` / `removeArea()` | Drive the corner editor from your own chrome |
| `canAddArea` / `canRemoveArea` | Whether those two will do anything |
| `selectArea(index)` | Make an area the one being edited |
| `renameArea(index, name)` | Name an area. Empty clears it. Stored on the area set |
| `saveAreas()` / `editAreas()` | Commit and measure; reopen the editor |
| `showMeasurements(on)` | Draw the sizes on the photo, or not |
| `getPhase()` | Where the customer is, paired with the `phase` event |
| `getCapabilities()` | What this instance is allowed to do |
| `reset()` | Back to the upload panel, abandoning anything in flight |
Methods that could be a no-op — `chooseFile`, `addArea`, `removeArea`, `selectArea`,
`renameArea`, `saveAreas`, `editAreas`, `visualize`, `showMeasurements` — return a boolean
saying whether they did anything. The setters and `reset()` return nothing.
`selectArea` matters more than it looks. Only the active area's handles can be grabbed, so
with two overlapping outlines on one photo, picking by index is the only reliable way to
say which one you mean.
With `ui="none"` **you own every control**, including the one that commits the areas.
Nothing calls `saveAreas()` for you, so without it the customer cannot finish.
## Events [#events]
`CustomEvent`s on the element, prefixed `viewa:`, bubbling and composed.
| Event | `detail` |
| ------------------- | --------------------------------------------------------------- |
| `ready` | `{}` — the element is upgraded and listening |
| `phase` | `{ phase, step, capabilities }` |
| `image-uploaded` | `{ id }` — accepted, not yet processed |
| `image-ready` | `{ id }` — processed and safe to render against |
| `areas-changed` | `{ count, active, canAdd, canRemove }` — fires on every drag |
| `areas-ready` | `{ id, areas }` — the API's measurement of them |
| `selection-changed` | `{ selection }` |
| `render-started` | `{ areaSet }` — the render's own id arrives with `render-ready` |
| `render-ready` | `{ id, url }` |
| `error` | `{ message, code }` |
```js
widget.addEventListener('viewa:render-ready', (event) => {
cartLine.renderId = event.detail.id
})
```
`areas-changed` and `areas-ready` are not the same thing. The first fires while the
customer is still dragging, before anything is sent; the second is the measurement that
came back. Bind enabled states to `phase`, which is emitted after the widget has redrawn,
so a listener reading `can` sees the new state rather than the old one.
**`error` is the single failure channel.** Job failures — image preprocessing, area
occlusion, render capture — arrive from the API on a `200` with an `error.code`, and go
out through this same event rather than a parallel set of `*-failed` ones. `message` is
always customer-safe copy; `code` is the stable thing to branch on. See
[Errors](/docs/ecommerce/concepts/errors).
## Next [#next]
* [Embedding](/docs/ecommerce/widget/embed) — the attributes, and splitting the journey
across pages
* [Theming](/docs/ecommerce/widget/theming) — the custom properties, and choosing which
controls the widget draws
# Theming (/docs/ecommerce/widget/theming)
The widget's styles live inside its shadow root, where a utility class from your theme
cannot reach them. So every knob is a **CSS custom property**, set on the element from
your own stylesheet. That is the whole theming contract.
```css
viewa-visualizer {
--viewa-aspect: 4 / 5;
--viewa-accent: #101012;
--viewa-radius: 14px;
}
```
Typography is deliberately not on the list. Type inherits from your page, so the
visualizer reads as part of the store rather than an iframe bolted on.
## Properties [#properties]
| Property | What it sets |
| ------------------------- | ------------------------------------------------------------------------------------------------- |
| `--viewa-aspect` | The shape of the widget, in every step. `1` by default |
| `--viewa-min-height` | A floor for the whole widget, for pinning it outright. `0` by default |
| `--viewa-accent` | The primary color: buttons, area outlines, handles, sliders |
| `--viewa-accent-contrast` | What sits on the accent, and label outlines over a dark photo |
| `--viewa-surface` | The widget's own surface |
| `--viewa-stage` | The ground a photo is letterboxed against. `transparent` by default, so the surface shows through |
| `--viewa-text` | Body text |
| `--viewa-muted` | Secondary text |
| `--viewa-border` | Hairlines and outlines |
| `--viewa-danger` | Error copy |
| `--viewa-radius` | Corner radius for the frame and buttons |
| `--viewa-handle-size` | The drawn corner dot. The *tap target* stays 44px whatever this is |
| `--viewa-font-size` | The base every type size is a multiple of. `1rem` by default |
The type scale is **one property rather than one per label**. Every size in the
widget is an `em` off `--viewa-font-size`, so raising it scales them together and
keeps the proportions the design has:
```css
viewa-visualizer { --viewa-font-size: 15px; }
```
`1rem` by default, which is what the sizes were relative to before it existed, so
setting nothing changes nothing. `inherit` makes the widget follow whatever its
container is set to.
**`--viewa-aspect` is the one that keeps your page still.** The widget is a square by
default and holds that shape from the upload panel through to the finished render, with
the photo contained inside it — so nothing below the widget moves as the customer works
through the steps. Set it to whatever your gallery slot is. `auto` gives back the older
behaviour of following each photo's own proportions, jumps included.
The area toolbar sits **inside** that shape rather than floating over the photo, so it can
never cover a corner the customer is trying to drag. The photo takes the space the toolbar
leaves; the two together always add up to the same height.
The ground a photo is letterboxed against is `--viewa-stage`, transparent by default so
the photo sits on the same surface as the rest of the widget. Set a colour only if you
want the photo lifted off the page; a dark one reads as a lightbox — as though your page
had been replaced by a viewer — where this is a photo inside a product gallery.
One thing is deliberately not themeable: the corner handles keep a 44px tap target however
small you draw them.
## Your logo, or none [#your-logo-or-none]
The upload panel carries the Viewa lockup by default. It is a large brand
statement on your product page, so you can replace it or remove it:
```html
```
A `data:` URI is worth preferring over a URL. The widget is a single file with
nothing to fetch, and an `
` pointing at another origin is a request that
can fail on its own — a logo that fails to load removes itself rather than
leaving a broken-image glyph, but a gap is still not what you asked for.
This covers the panel mark. The small "powered by viewa" attribution underneath
is separate, and not currently configurable.
## Choosing what is drawn [#choosing-what-is-drawn]
`ui` controls which pieces the widget draws for itself. It matters because a product
page's gallery slot is mostly photo, and everything the widget adds competes with that.
| Part | What it draws | Default |
| -------------- | ---------------------------------------------------------------------------------------------------------- | ------- |
| `chrome` | The layer of controls over the photo. The umbrella — with it off, none of the parts below are drawn either | On |
| `controls` | The pad below the photo | On |
| `zoom` | The zoom button and its slider | On |
| `measurements` | The sizes drawn on the photo, and the ruler that toggles them | On |
| `compare` | The before/after wipe over a finished render | On |
| `download` | The save-photo button | On |
| `change-photo` | The change-photo button | On |
| `coach` | The first-run "align your area" tips | **Off** |
| `photos` | "Use a photo from before" on the upload screen | **Off** |
| `history` | The previous-renders strip | **Off** |
```html
```
**Declaring anything replaces the whole set** rather than adding to the default. That is
what makes the fine-grained parts usable as subtractions: write out the ones you want. It
also means `ui="chrome"` keeps meaning exactly what it means today, rather than quietly
picking up whatever gets added in a later release.
A value the widget does not recognize falls back to the default rather than to nothing — a
typo in a template should not strip the controls a customer needs to finish.
With `ui="none"` you own every control, including the one that commits the areas. See
the [JS API](/docs/ecommerce/widget/js-api#methods) for what you need to drive.
## How far this goes [#how-far-this-goes]
Four layers, loosest first. Most storefronts need the first two.
| To change | Use |
| ---------------------------------- | ----------------------------------------------------------- |
| Colours, radius, shape, type scale | The `--viewa-*` properties |
| Your logo instead of ours, or none | `logo` |
| Which controls exist at all | `ui`, per part or `none` |
| The controls themselves | `ui="none"` and the [JS API](/docs/ecommerce/widget/js-api) |
`ui="none"` is the escape hatch. The widget draws the photo and the corner
editor; your theme draws everything else against `getStatus()` and the JS API.
That covers a fully bespoke interface without giving up the pipeline — the
upload, the measurement, the render and the visitor token all still work.
Past that is a direct API integration, and the line is worth naming: the widget
exists so you do not have to hold the pipeline's state machine. Once you want an
interface it cannot express, you want [the API](/docs/ecommerce/concepts/images),
not more attributes.
## Next [#next]
* [Embedding](/docs/ecommerce/widget/embed) — the attributes, and splitting the journey
across pages
* [JS API](/docs/ecommerce/widget/js-api) — pushing selections in, reading state back out
# Elementos de implementación (/docs/surface/es/elements)
Estos elementos pueden usarse de forma individual, pero las implementaciones más
exitosas combinan varios a lo largo del recorrido del cliente.
## 1. Botones en la barra de navegación (escritorio y móvil) [#1-navigation-bar-buttons-desktop--mobile]
**Objetivo:** visibilidad permanente y acceso fácil.
Agregar un botón "Visualizar" o "Visualízalo en tu casa" a la navegación principal
garantiza que sus clientes siempre sepan que la función existe.
Recomendamos textos que den contexto, como:
* ✔ "Visualízalo en tu casa"
* ✔ "Sube tu foto"
* ✔ "Míralo en tu espacio"
En móvil, el botón puede aparecer:
* Como botón en el encabezado
* Dentro del menú de navegación
* Acompañado de un texto descriptivo
**Buena práctica:** abra un popover con contexto o enlace a una página de destino
dedicada, en lugar de abrir el visualizador directamente sin ninguna explicación.
### Ejemplos en escritorio [#desktop-examples]
### Ejemplos en móvil [#mobile-examples]
## 2. Banners con llamado a la acción contextual [#2-contextual-cta-banners]
**Objetivo:** informar antes de la acción.
Los banners destacados en páginas clave (página de inicio, páginas de categoría,
colecciones de productos) ayudan a explicar:
* Qué hace la herramienta
* Que los usuarios pueden subir una foto de su propio espacio
* Qué podrán personalizar
Texto de ejemplo:
> Sube una foto de tu habitación para explorar estilos, colores y materiales en tu
> propio espacio.
Estos banners mejoran de forma notable la tasa de clics frente a los botones aislados.
## 3. Popovers y ventanas introductorias [#3-popovers--intro-modals]
**Objetivo:** dar claridad inmediata.
En lugar de enlazar directamente al visualizador, un popover puede:
* Explicar cómo funciona
* Mostrar una imagen de vista previa
* Describir el proceso en 3 pasos (Subir → Seleccionar la zona → Personalizar)
* Reforzar que la experiencia funciona en móvil
Los popovers pueden:
* Abrirse manualmente desde la navegación
* Mostrarse automáticamente a quienes visitan el sitio por primera vez
Este enfoque reduce las dudas de los usuarios y les da más confianza al abrir la
herramienta.
## 4. Página de destino dedicada [#4-dedicated-landing-page]
**Objetivo:** preparar por completo al usuario antes de abrir la herramienta.
Una página de destino dedicada es uno de los enfoques de implementación más eficaces.
Le permite:
* Explicar cómo funciona el visualizador
* Mostrar capturas de pantalla o videos de demostración
* Destacar los beneficios (confianza, mejores decisiones, selección más rápida)
* Mejorar el SEO para términos relacionados con sus productos (por ejemplo,
"visualizador de cortinas" o "ver cortinas en mi habitación")
Esta página debe comunicar con claridad:
* Sube una foto de tu espacio
* Selecciona la zona
* Explora acabados y estilos al instante
Incluya botones de llamado a la acción visibles, como:
* Visualízalo en tu casa
* Sube tu foto ahora
Esta página puede enlazarse desde la navegación, los banners y las páginas de producto.
## 5. Botones "Visualizar" en las páginas de producto [#5-product-page-visualise-buttons]
**Objetivo:** aumentar la confianza al solicitar una cotización y la calidad de los
contactos.
Muchas veces el objetivo del visualizador no es la compra inmediata, sino que los
clientes se sientan lo bastante seguros como para enviar una consulta.
Agregar un botón "Visualízalo en tu casa" directamente en las páginas de detalle de
producto permite a los usuarios ver una vista previa del producto exacto que están
considerando antes de contactar a su equipo.
# Preguntas frecuentes (/docs/surface/es/faq)
## ¿Podemos incrustar el iframe dentro de una página existente? [#can-we-embed-the-iframe-within-an-existing-page]
No. La incrustación no es compatible: el visualizador ofrece el mayor beneficio al
usuario cuando aprovecha al máximo el espacio de la pantalla, así que enlace
directamente a él. Consulte [Enlace, no incruste](/docs/surface/es#link-out-dont-embed).
## ¿Podemos personalizar el aspecto visual? [#can-we-customise-the-look-and-feel]
Sí. El visualizador permite una amplia personalización del tema. Comuníquese con Viewa
para conocer los detalles.
## ¿Podemos medir el uso y la participación? [#can-we-track-usage-and-engagement]
Sí. Puede agregar parámetros de seguimiento de campañas a sus enlaces al visualizador.
También ofrecemos un portal de seguimiento y analítica que registra el comportamiento de
los usuarios dentro de la plataforma Viewa.
## ¿Podemos alojar el visualizador en nuestro dominio? [#can-we-host-the-visualiser-on-our-domain]
Los clientes con necesidades avanzadas pueden solicitar un subdominio de marca blanca
(por ejemplo, `visualiser.yoursite.com`). Contáctenos para obtener más información.
## ¿Necesita ayuda? [#need-help]
Para recibir asistencia con la integración, la configuración o las buenas prácticas,
comuníquese con el soporte de Viewa en [support@viewa.com](mailto:support@viewa.com).
# Guía de implementación (/docs/surface/es)
La plataforma Viewa Surface permite que sus clientes interactúen más a fondo con sus
productos gracias a funciones avanzadas de visualización e interacción, lo que se
traduce en más conversiones de venta.
Al visitar su sitio web, los compradores potenciales pueden usar el visualizador para
tomar una foto de su espacio. El software analiza la foto, guía al usuario para
seleccionar la zona de interés y le permite visualizar sus productos en ese lugar. Los
usuarios pueden cambiar acabados y materiales en tiempo real e imaginar las opciones más
adecuadas para su entorno.
La plataforma también ofrece una selección de espacios de inspiración ya diseñados, en
los que los clientes pueden integrar sus productos para evaluar cómo se ven y cómo
encajan.
Esta guía describe los pasos para implementar el visualizador en su sitio.
Su visualizador se encuentra en una **URL de instancia** dedicada, que Viewa le
proporciona durante el proceso de incorporación. Donde esta guía muestre un marcador
como `your-instance`, sustitúyalo por los datos de su propia instancia.
## Estrategia de implementación y preparación de los usuarios [#implementation-strategy--user-preparation]
El éxito del visualizador no depende solo de la integración: depende de qué tan
claramente entienden sus clientes lo que la herramienta les permite hacer.
Nuestros análisis en varias implementaciones con clientes muestran que:
* Un único botón de "Visualizador" sin contexto puede generar poca participación.
* Es necesario preparar e informar a los usuarios antes de que abran la herramienta.
* Comunicar claramente que pueden subir una foto de su propio espacio aumenta de forma
significativa la tasa de interacción.
* Las implementaciones con varios puntos de contacto (navegación + páginas de producto +
banners) superan de forma constante a las que usan un solo enlace.
## Enlace, no incruste [#link-out-dont-embed]
Nuestros clientes con mejores resultados enlazan directamente al visualizador en lugar
de incrustarlo en su sitio. Un enlace directo le da al visualizador toda la pantalla,
evita problemas de compatibilidad de los iframes entre dispositivos y ofrece a sus
clientes una experiencia más rápida y confiable.
Recomendamos ampliamente este enfoque para todas las implementaciones. Todos los
[elementos de integración](/docs/surface/es/elements) están pensados para enlazar al
visualizador, no para incrustarlo.
Para obtener el máximo rendimiento, recomendamos un plan de implementación estructurado
que use los elementos de esta guía. Las implementaciones más exitosas combinan varios
elementos a lo largo del recorrido del cliente.
# Preselección de productos y materiales (/docs/surface/es/preloading)
**Objetivo:** llevar a sus visitantes directamente al producto que ya estaban viendo.
Cualquier enlace a su visualizador puede incluir parámetros opcionales que preseleccionan
un producto, un material o ambos. El visitante se salta el selector y ve su elección en
cuanto se abre el visualizador. Esto combina muy bien con los
[botones "Visualizar" en las páginas de producto](/docs/surface/es/elements#5-product-page-visualise-buttons).
## Parámetros [#parameters]
| Parámetro | Preselecciona | Acepta |
| ------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `dp` (o `product`) | Un producto | El slug o el SKU del producto en su catálogo de Viewa, o uno de sus propios identificadores registrado como [alias de catálogo](#catalog-aliases) |
| `dm` (o `sku`) | Un material | El slug o el SKU del material, o un alias de catálogo |
`product` y `sku` son nombres alternativos de `dp` y `dm`, para integraciones que ya
envían esos parámetros. Si se incluyen ambas formas, prevalecen `dp` y `dm`.
## Ejemplos [#examples]
Sustituya la URL de su instancia y los identificadores por los suyos:
```html
Visualízalo en tu casa
Visualízalo en tu casa
Visualízalo en tu casa
Míralo en una sala
Visualízalo en tu casa
```
Un material solo se aplica si el producto seleccionado lo ofrece. Si se indica solo el
material, se aplica cuando el visitante elige un producto que lo ofrece.
Abrir o no el visualizador en una pestaña nueva depende del recorrido que usted quiera.
Con `target="_blank"` su página permanece abierta detrás, así que los visitantes pueden
cerrar el visualizador y seguir donde se quedaron; sin él, el visualizador reemplaza su
página y tienen que regresar. Cuando use `target="_blank"`, agregue también
`rel="noopener"`.
## Alias de catálogo [#catalog-aliases]
Es poco común que su sitio use los slugs y SKU de Viewa. Un **alias de catálogo** asocia
uno de sus propios identificadores con su catálogo de Viewa, para que sus enlaces puedan
usar los valores que su plataforma ya tiene: el SKU de una variante de WooCommerce, un
handle de Shopify o el slug de una categoría.
Cada alias asocia un identificador con uno de los siguientes:
| Destino del alias | Úselo para | Ejemplo |
| ------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------- |
| Un producto | Un identificador de categoría o de producto de su sitio | `roller-blinds` → Roller Blind |
| Un material | El SKU de una tela o de un color | `FAB-1001` → Linen Blockout, White |
| Un producto y un material | Un SKU que identifica ambos a la vez | `RB-1001-WHT` → Roller Blind en Linen Blockout, White |
| Una línea de materiales | Se registra por completitud: el SKU de una línea no preselecciona nada | `LINEN-BO` |
Un SKU que identifica tanto un producto como un material preselecciona el elemento que
pide cada parámetro, así que envíelo en ambos: `?product=RB-1001-WHT&sku=RB-1001-WHT`.
Cómo se comparan los alias:
* La comparación no distingue entre mayúsculas y minúsculas e ignora los espacios al
inicio y al final; los alias son por instancia.
* Un alias **tiene prioridad** sobre un slug o SKU de Viewa con el mismo valor.
* Un alias que apunta a algo que el parámetro no puede usar (un alias de material en
`dp`, o el SKU de una línea en cualquier parámetro) no coincide con nada. No se recurre
al catálogo como alternativa.
Viewa configura sus alias por usted: envíenos los identificadores que usa su sitio (lo
ideal es un feed o una exportación de productos) y los asociaremos con su catálogo.
## Información importante [#things-to-know]
* Los valores no distinguen entre mayúsculas y minúsculas.
* Un producto o material no reconocido se ignora: el visualizador se abre con su
configuración predeterminada en lugar de mostrar un error, así que un enlace a un SKU
descontinuado sigue funcionando.
* La selección se mantiene durante el resto de la sesión del navegador, incluso al
navegar dentro del visualizador.
* Algunos CMS y editores de correo electrónico codifican el carácter `&`. Si solo se
aplica el primer parámetro, revise el `&` entre ambos.
* La preselección todavía no es compatible con instancias hijas, es decir, un
visualizador que comparte el catálogo de otra instancia, como el sitio de un
distribuidor o de una sucursal. Los enlaces a esas instancias se abren con la
configuración predeterminada. Contáctenos si la necesita.
# Implementation Elements (/docs/surface/implementation/elements)
These elements can be used individually, but the most successful deployments combine
multiple elements across the customer journey.
## 1. Navigation Bar Buttons (Desktop & Mobile) [#1-navigation-bar-buttons-desktop--mobile]
**Purpose:** Persistent visibility and easy access.
Adding a "Visualise" or "Visualise at Home" button to your main navigation ensures
customers always know the feature exists.
We recommend context-driven language such as:
* ✔ "Visualise in Your Home"
* ✔ "Upload Your Photo"
* ✔ "See It in Your Space"
On mobile, this can appear:
* As a header button
* Within the navigation drawer
* With supporting descriptive text
**Best practice:** trigger a contextual popover or link to a dedicated landing page
rather than launching directly without explanation.
### Desktop examples [#desktop-examples]
### Mobile examples [#mobile-examples]
## 2. Contextual CTA Banners [#2-contextual-cta-banners]
**Purpose:** Educate before action.
Feature banners on key pages (homepage, category pages, product collections) help
explain:
* What the tool does
* That users can upload their own photo
* What they will be able to customise
Example copy:
> Upload a photo of your room to explore styles, colours and materials in your own
> space.
These banners significantly improve click-through rates compared to standalone buttons.
## 3. Popovers & Intro Modals [#3-popovers--intro-modals]
**Purpose:** Provide immediate clarity.
Instead of linking directly to the visualiser, a popover can:
* Explain how it works
* Show a preview image
* Outline the 3-step process (Upload → Select Area → Customise)
* Reinforce that the experience works on mobile
Popovers can be:
* Manually triggered via navigation
* Automatically shown to first-time visitors
This approach reduces user hesitation and increases launch confidence.
## 4. Dedicated Landing Page [#4-dedicated-landing-page]
**Purpose:** Fully prepare the user before launch.
A dedicated landing page is one of the most effective implementation approaches. It
allows you to:
* Explain how the visualiser works
* Showcase screenshots or video walkthroughs
* Highlight benefits (confidence, decision-making, faster selection)
* Improve SEO around terms relevant to your products (for example "blinds visualiser"
or "see blinds in my room")
This page should clearly communicate:
* Upload your own photo
* Select your area
* Explore finishes and styles instantly
Include strong CTA buttons such as:
* Visualise in Your Home
* Upload Your Photo Now
This page can be linked from navigation, banners and product pages.
## 5. Product Page "Visualise" Buttons [#5-product-page-visualise-buttons]
**Purpose:** Increase enquiry confidence and lead quality.
The goal of the visualiser is often not immediate purchase — it is to help customers
feel confident enough to submit an enquiry.
Adding a "Visualise in Your Home" button directly on product detail pages allows users
to preview the exact product they are considering before contacting your team.
# FAQ (/docs/surface/implementation/faq)
## Can we embed the iframe within an existing page? [#can-we-embed-the-iframe-within-an-existing-page]
No. Embedding isn't supported — the visualiser provides the greatest benefit to the
user when its screen space is maximised, so link to it directly. See
[Link Out, Don't Embed](/docs/surface/implementation#link-out-dont-embed).
## Can we customise the look and feel? [#can-we-customise-the-look-and-feel]
Yes. The visualiser supports extensive theming. Reach out to Viewa for details.
## Can we track usage and engagement? [#can-we-track-usage-and-engagement]
Yes. You can add campaign tracking parameters to your links to the visualiser. We also
provide a tracking and analytics portal to capture user behaviour within the Viewa
platform itself.
## Can we host the visualiser on our domain? [#can-we-host-the-visualiser-on-our-domain]
Advanced clients may request a white-labeled subdomain (for example,
`visualiser.yoursite.com`). Contact us to learn more.
## Need help? [#need-help]
For assistance with integration, configuration, or best practices, contact Viewa
Support at [support@viewa.com](mailto:support@viewa.com).
# Implementation Guide (/docs/surface/implementation)
The Viewa Surface platform empowers your customers to engage more deeply with your
products by offering enhanced visualisation and interaction capabilities, leading to
increased sales conversions.
When visiting your website, potential buyers can use the visualiser to capture an
image of their space. The software analyses the photo, guides the user to select their
point of interest, and lets them visualise a range of your products in place. Users can
modify finishes and materials in real time, envisioning the most suitable options for
their environment.
The platform also provides a selection of pre-designed, inspirational spaces where
customers can seamlessly integrate your products to gauge their fit and aesthetics.
This guide outlines the steps for implementing the visualiser on your site.
This guide is also available in [Italiano](/docs/surface/it) and
[Español](/docs/surface/es).
Your visualiser lives at a dedicated **instance URL**, provided by Viewa during
onboarding. Wherever this guide shows a placeholder like `your-instance`, substitute
your own instance details.
## Implementation Strategy & User Preparation [#implementation-strategy--user-preparation]
The success of the visualiser is not determined by integration alone — it is driven by
how clearly customers understand what the tool allows them to do.
Our research across multiple client deployments has shown:
* A single "Visualiser" button without context may lead to low engagement.
* Users must be prepared and educated before launching the tool.
* Clearly communicating that users can upload their own photo significantly increases
interaction rates.
* Implementations that include multiple touchpoints (navigation + product pages +
banners) consistently outperform single-link approaches.
## Link Out, Don't Embed [#link-out-dont-embed]
Our highest-performing clients link directly to the visualiser rather than embedding it
within their site. A direct link gives the visualiser full screen real estate, avoids
iframe compatibility issues across devices, and delivers a faster, more reliable
experience for your customers.
We strongly recommend this approach for all implementations. The
[integration elements](/docs/surface/implementation/elements) are all designed around
linking out to the visualiser — not embedding it.
To maximise performance, we recommend a structured implementation plan using the
elements in this guide. The most successful deployments combine multiple elements
across the customer journey.
# Pre-selecting Products & Materials (/docs/surface/implementation/preloading)
**Purpose:** drop visitors straight into the product they were already looking at.
Any link to your visualiser can carry optional parameters that pre-select a product, a
material, or both. The visitor skips the picker and sees their choice as soon as the
visualiser opens. This pairs naturally with
[product page "Visualise" buttons](/docs/surface/implementation/elements#5-product-page-visualise-buttons).
## Parameters [#parameters]
| Parameter | Pre-selects | Accepts |
| ------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `dp` (or `product`) | A product | The product's slug or SKU from your Viewa catalogue, or one of your own identifiers registered as a [catalog alias](#catalog-aliases) |
| `dm` (or `sku`) | A material | The material's slug or SKU, or a catalog alias |
`product` and `sku` are alternative names for `dp` and `dm`, for integrations that
already send those. If both forms are present, `dp` and `dm` win.
## Examples [#examples]
Substitute your own instance URL and identifiers:
```html
Visualise in Your Home
Visualise in Your Home
Visualise in Your Home
See it in a Living Room
Visualise in Your Home
```
A material is only applied if the selected product offers it. With a material on its
own, it is applied once the visitor picks a product that offers it.
Whether to open the visualiser in a new tab depends on the journey you want. With
`target="_blank"` your page stays open behind it, so visitors can close the visualiser
and carry on where they left off; without it, the visualiser replaces your page and
they have to navigate back. When you do use `target="_blank"`, add `rel="noopener"`
alongside it.
## Catalog aliases [#catalog-aliases]
Your site rarely uses Viewa's slugs and SKUs. A **catalog alias** maps one of your own
identifiers onto your Viewa catalogue, so your links can use the values your platform
already has — a WooCommerce variation SKU, a Shopify handle, or a category slug.
Each alias maps one identifier to one of:
| Alias target | Use it for | Example |
| ------------------------ | ----------------------------------------------------------- | ----------------------------------------------------- |
| A product | A category or product identifier from your site | `roller-blinds` → Roller Blind |
| A material | A fabric or colour SKU | `FAB-1001` → Linen Blockout, White |
| A product and a material | A SKU that names both at once | `RB-1001-WHT` → Roller Blind in Linen Blockout, White |
| A material range | Recorded for completeness — a range SKU pre-selects nothing | `LINEN-BO` |
A SKU that names both a product and a material pre-selects whichever one the
parameter asks for, so send it in both: `?product=RB-1001-WHT&sku=RB-1001-WHT`.
How aliases are matched:
* Matching ignores case and surrounding spaces, and aliases are per instance.
* An alias **takes priority** over a Viewa slug or SKU with the same value.
* An alias that points at something the parameter can't use — a material alias in
`dp`, or a range SKU anywhere — matches nothing. It does not fall back to a catalogue
match.
Viewa sets up your aliases for you — send us the identifiers your site uses (a product
feed or export is ideal) and we'll map them onto your catalogue.
## Things to know [#things-to-know]
* Values are not case-sensitive.
* An unrecognised product or material is ignored — the visualiser opens with its usual
defaults rather than an error, so a link to a discontinued SKU degrades gracefully.
* The selection persists for the rest of the browser session, surviving navigation
inside the visualiser.
* Some CMS and email editors escape ampersands. If only the first parameter is applied,
check the `&` between them.
* Pre-selection isn't yet supported on child instances — a visualiser that shares
another instance's catalogue, such as a dealer or branch site. Links there open with
the usual defaults. Contact us if you need it.
# Elementi di implementazione (/docs/surface/it/elements)
Questi elementi possono essere usati singolarmente, ma le implementazioni di maggior
successo ne combinano diversi lungo il percorso del cliente.
## 1. Pulsanti nella barra di navigazione (desktop e mobile) [#1-navigation-bar-buttons-desktop--mobile]
**Obiettivo:** visibilità costante e accesso immediato.
Aggiungere un pulsante "Visualizza" o "Visualizza a casa tua" alla navigazione
principale garantisce che i clienti sappiano sempre che la funzione esiste.
Consigliamo formulazioni che diano contesto, come:
* ✔ "Visualizza a casa tua"
* ✔ "Carica la tua foto"
* ✔ "Guardalo nel tuo ambiente"
Su mobile, il pulsante può comparire:
* Come pulsante nell'intestazione
* All'interno del menu di navigazione
* Accompagnato da un testo descrittivo
**Buona pratica:** aprire un popover contestuale o collegare una landing page dedicata,
invece di avviare direttamente lo strumento senza spiegazioni.
### Esempi desktop [#desktop-examples]
### Esempi mobile [#mobile-examples]
## 2. Banner con call to action contestuale [#2-contextual-cta-banners]
**Obiettivo:** informare prima dell'azione.
I banner nelle pagine principali (homepage, pagine di categoria, collezioni di prodotti)
aiutano a spiegare:
* Che cosa fa lo strumento
* Che è possibile caricare una foto del proprio ambiente
* Che cosa si potrà personalizzare
Testo di esempio:
> Carica una foto della tua stanza per esplorare stili, colori e materiali nel tuo
> ambiente.
Questi banner migliorano sensibilmente il tasso di clic rispetto ai soli pulsanti.
## 3. Popover e finestre introduttive [#3-popovers--intro-modals]
**Obiettivo:** offrire chiarezza immediata.
Invece di collegare direttamente il visualizzatore, un popover può:
* Spiegare come funziona
* Mostrare un'immagine di anteprima
* Illustrare il processo in 3 passaggi (Carica → Seleziona l'area → Personalizza)
* Ribadire che l'esperienza funziona anche su mobile
I popover possono essere:
* Aperti manualmente dalla navigazione
* Mostrati automaticamente ai visitatori alla prima visita
Questo approccio riduce l'esitazione degli utenti e aumenta la fiducia al momento
dell'avvio.
## 4. Landing page dedicata [#4-dedicated-landing-page]
**Obiettivo:** preparare completamente l'utente prima dell'avvio.
Una landing page dedicata è uno degli approcci di implementazione più efficaci.
Consente di:
* Spiegare come funziona il visualizzatore
* Mostrare screenshot o video dimostrativi
* Evidenziare i vantaggi (sicurezza nella scelta, decisioni più consapevoli, selezione
più rapida)
* Migliorare la SEO per termini pertinenti ai propri prodotti (ad esempio "visualizzatore
tende" o "vedere le tende nella mia stanza")
La pagina dovrebbe comunicare chiaramente:
* Carica una foto del tuo ambiente
* Seleziona l'area
* Esplora subito finiture e stili
Includere pulsanti di call to action ben visibili, come:
* Visualizza a casa tua
* Carica subito la tua foto
La pagina può essere collegata dalla navigazione, dai banner e dalle pagine prodotto.
## 5. Pulsanti "Visualizza" nelle pagine prodotto [#5-product-page-visualise-buttons]
**Obiettivo:** aumentare la fiducia nelle richieste di informazioni e la qualità dei
contatti.
Spesso l'obiettivo del visualizzatore non è l'acquisto immediato, ma aiutare i clienti a
sentirsi abbastanza sicuri da inviare una richiesta di informazioni.
Aggiungere un pulsante "Visualizza a casa tua" direttamente nelle pagine di dettaglio
prodotto consente agli utenti di vedere in anteprima proprio il prodotto che stanno
valutando, prima di contattare il vostro team.
# Domande frequenti (/docs/surface/it/faq)
## È possibile incorporare l'iframe in una pagina esistente? [#can-we-embed-the-iframe-within-an-existing-page]
No. L'incorporamento non è supportato: il visualizzatore offre il massimo beneficio
all'utente quando dispone di tutto lo spazio dello schermo, quindi va collegato
direttamente. Vedere [Collegare, non incorporare](/docs/surface/it#link-out-dont-embed).
## È possibile personalizzare l'aspetto grafico? [#can-we-customise-the-look-and-feel]
Sì. Il visualizzatore supporta un'ampia personalizzazione del tema. Contattate Viewa per
i dettagli.
## È possibile monitorare l'utilizzo e il coinvolgimento? [#can-we-track-usage-and-engagement]
Sì. È possibile aggiungere parametri di tracciamento delle campagne ai link verso il
visualizzatore. Mettiamo inoltre a disposizione un portale di tracciamento e analisi
che registra il comportamento degli utenti all'interno della piattaforma Viewa.
## È possibile ospitare il visualizzatore sul nostro dominio? [#can-we-host-the-visualiser-on-our-domain]
I clienti con esigenze avanzate possono richiedere un sottodominio white label (ad
esempio `visualiser.yoursite.com`). Contattateci per saperne di più.
## Serve aiuto? [#need-help]
Per assistenza su integrazione, configurazione o buone pratiche, contattate il supporto
Viewa all'indirizzo [support@viewa.com](mailto:support@viewa.com).
# Guida all'implementazione (/docs/surface/it)
La piattaforma Viewa Surface permette ai clienti di interagire più a fondo con i
prodotti grazie a funzionalità avanzate di visualizzazione e interazione, con un
conseguente aumento delle conversioni.
Visitando il sito, i potenziali acquirenti possono usare il visualizzatore per
fotografare il proprio ambiente. Il software analizza la foto, guida l'utente nella
selezione dell'area di interesse e consente di visualizzare in contesto una gamma di
prodotti. Gli utenti possono modificare finiture e materiali in tempo reale e immaginare
le opzioni più adatte al proprio ambiente.
La piattaforma offre inoltre una selezione di ambienti di ispirazione già pronti, in cui
i clienti possono inserire i prodotti per valutarne la resa e l'estetica.
Questa guida descrive i passaggi per implementare il visualizzatore sul proprio sito.
Il visualizzatore è raggiungibile a un **URL di istanza** dedicato, fornito da Viewa
durante l'onboarding. Ovunque questa guida mostri un segnaposto come `your-instance`,
va sostituito con i dati della propria istanza.
## Strategia di implementazione e preparazione degli utenti [#implementation-strategy--user-preparation]
Il successo del visualizzatore non dipende solo dall'integrazione, ma soprattutto da
quanto chiaramente i clienti comprendono che cosa lo strumento consente di fare.
Le nostre analisi su diverse implementazioni presso clienti hanno evidenziato che:
* Un semplice pulsante "Visualizzatore" senza contesto può generare un coinvolgimento
ridotto.
* Gli utenti vanno preparati e informati prima di avviare lo strumento.
* Comunicare chiaramente che è possibile caricare una foto del proprio ambiente aumenta
in modo significativo il tasso di interazione.
* Le implementazioni con più punti di contatto (navigazione + pagine prodotto + banner)
ottengono costantemente risultati migliori rispetto a quelle con un unico link.
## Collegare, non incorporare [#link-out-dont-embed]
I clienti con i risultati migliori collegano direttamente il visualizzatore invece di
incorporarlo nel proprio sito. Un link diretto lascia al visualizzatore l'intero spazio
dello schermo, evita problemi di compatibilità degli iframe sui diversi dispositivi e
offre ai clienti un'esperienza più veloce e affidabile.
Consigliamo vivamente questo approccio per tutte le implementazioni. Gli
[elementi di integrazione](/docs/surface/it/elements) sono tutti pensati per collegare
il visualizzatore, non per incorporarlo.
Per ottenere i migliori risultati, consigliamo un piano di implementazione strutturato
che utilizzi gli elementi descritti in questa guida. Le implementazioni di maggior
successo combinano più elementi lungo il percorso del cliente.
# Preselezione di prodotti e materiali (/docs/surface/it/preloading)
**Obiettivo:** portare i visitatori direttamente al prodotto che stavano già guardando.
Qualsiasi link al visualizzatore può includere parametri facoltativi che preselezionano
un prodotto, un materiale o entrambi. Il visitatore salta la fase di scelta e vede la
propria selezione non appena il visualizzatore si apre. Questa funzione si abbina
naturalmente ai
[pulsanti "Visualizza" nelle pagine prodotto](/docs/surface/it/elements#5-product-page-visualise-buttons).
## Parametri [#parameters]
| Parametro | Preseleziona | Accetta |
| ----------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `dp` (oppure `product`) | Un prodotto | Lo slug o lo SKU del prodotto nel catalogo Viewa, oppure un proprio identificativo registrato come [alias di catalogo](#catalog-aliases) |
| `dm` (oppure `sku`) | Un materiale | Lo slug o lo SKU del materiale, oppure un alias di catalogo |
`product` e `sku` sono nomi alternativi di `dp` e `dm`, pensati per le integrazioni che
già inviano questi parametri. Se sono presenti entrambe le forme, prevalgono `dp` e `dm`.
## Esempi [#examples]
Sostituire l'URL dell'istanza e gli identificativi con i propri:
```html
Visualizza a casa tua
Visualizza a casa tua
Visualizza a casa tua
Guardalo in un soggiorno
Visualizza a casa tua
```
Un materiale viene applicato solo se il prodotto selezionato lo prevede. Se si indica
solo il materiale, questo viene applicato quando il visitatore sceglie un prodotto che
lo prevede.
Aprire o meno il visualizzatore in una nuova scheda dipende dal percorso desiderato.
Con `target="_blank"` la pagina resta aperta in secondo piano, così i visitatori
possono chiudere il visualizzatore e riprendere da dove avevano lasciato; senza, il
visualizzatore sostituisce la pagina e i visitatori devono tornare indietro. Quando si
usa `target="_blank"`, aggiungere anche `rel="noopener"`.
## Alias di catalogo [#catalog-aliases]
Raramente un sito usa gli slug e gli SKU di Viewa. Un **alias di catalogo** associa uno
dei propri identificativi al catalogo Viewa, in modo che i link possano usare i valori
già presenti sulla propria piattaforma: uno SKU di variante WooCommerce, un handle
Shopify o lo slug di una categoria.
Ogni alias associa un identificativo a uno dei seguenti elementi:
| Destinazione dell'alias | Da usare per | Esempio |
| -------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------- |
| Un prodotto | Un identificativo di categoria o di prodotto del proprio sito | `roller-blinds` → Roller Blind |
| Un materiale | Lo SKU di un tessuto o di un colore | `FAB-1001` → Linen Blockout, White |
| Un prodotto e un materiale | Uno SKU che li identifica entrambi | `RB-1001-WHT` → Roller Blind in Linen Blockout, White |
| Una gamma di materiali | Registrato per completezza: lo SKU di una gamma non preseleziona nulla | `LINEN-BO` |
Uno SKU che identifica sia un prodotto sia un materiale preseleziona l'elemento
richiesto dal parametro, quindi va inviato in entrambi:
`?product=RB-1001-WHT&sku=RB-1001-WHT`.
Come vengono confrontati gli alias:
* Il confronto ignora maiuscole/minuscole e gli spazi iniziali e finali; gli alias
valgono per singola istanza.
* Un alias **ha la precedenza** su uno slug o uno SKU Viewa con lo stesso valore.
* Un alias che punta a qualcosa che il parametro non può usare (un alias di materiale in
`dp`, oppure lo SKU di una gamma in qualsiasi parametro) non trova corrispondenze. Non
si ricorre al catalogo come alternativa.
Gli alias vengono configurati da Viewa: è sufficiente inviarci gli identificativi usati
sul proprio sito (l'ideale è un feed o un'esportazione dei prodotti) e li associeremo al
vostro catalogo.
## Da sapere [#things-to-know]
* I valori non distinguono tra maiuscole e minuscole.
* Un prodotto o un materiale non riconosciuto viene ignorato: il visualizzatore si apre
con le impostazioni predefinite invece di mostrare un errore, quindi un link a uno SKU
fuori produzione continua a funzionare.
* La selezione resta valida per il resto della sessione del browser, anche navigando
all'interno del visualizzatore.
* Alcuni CMS ed editor di email codificano il carattere `&`. Se viene applicato solo il
primo parametro, controllare la `&` tra i due.
* La preselezione non è ancora supportata nelle istanze figlie, ovvero i visualizzatori
che condividono il catalogo di un'altra istanza, come il sito di un rivenditore o di
una filiale. I link verso queste istanze si aprono con le impostazioni predefinite.
Contattateci se avete bisogno di questa funzione.