# 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] Ejemplo de botón en el encabezado de escritorio con etiqueta contextual Ubicación alternativa del botón en el encabezado de escritorio Implementación como elemento del menú de navegación de escritorio ### Ejemplos en móvil [#mobile-examples] Ejemplo de botón en el encabezado móvil Estilo alternativo del botón en el encabezado móvil Elemento del menú de navegación móvil ## 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. Ejemplo de un banner informativo Banner destacado con imágenes de apoyo y un llamado a la acción claro ## 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. Ejemplo de popover que explica el funcionamiento del visualizador ## 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. Ejemplo de página de destino dedicada con una explicación clara y llamado a la acción ## 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. Página de detalle de producto con botón Visualizar integrado # 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] Desktop header button example with contextual label Alternative desktop header button placement Desktop navigation menu item implementation ### Mobile examples [#mobile-examples] Mobile header button example Alternative mobile header button style Mobile navigation drawer menu item ## 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. Example of a contextual callout banner Feature banner with supporting imagery and clear CTA ## 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. Example popover dialog explaining the visualiser workflow ## 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. Example dedicated landing page with clear explanation and CTA ## 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. Product detail page with integrated Visualise button # 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] Esempio di pulsante nell'intestazione desktop con etichetta contestuale Posizionamento alternativo del pulsante nell'intestazione desktop Implementazione come voce del menu di navigazione desktop ### Esempi mobile [#mobile-examples] Esempio di pulsante nell'intestazione mobile Stile alternativo del pulsante nell'intestazione mobile Voce nel menu di navigazione mobile ## 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. Esempio di banner informativo Banner con immagini di supporto e call to action chiara ## 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. Esempio di popover che spiega il funzionamento del visualizzatore ## 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. Esempio di landing page dedicata con spiegazione chiara e call to action ## 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. Pagina di dettaglio prodotto con pulsante Visualizza integrato # 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.