seeonwall.com
Log in

Documentation

Everything you need to integrate seeonwall.com into your online shop.

Using Shopify?Install SeeOnWall from the Shopify App Store and follow our step-by-step setup guide — no code required.Open the guideUsing WordPress & WooCommerce?Install the free SeeOnWall plugin and follow our step-by-step setup guide — no code required.Open the guideSelling on a marketplace?Marketplaces don't allow embedded scripts. Generate a preview link instead — a plain URL you paste into your listing description.Open the guide

Quick Start

Get up and running in under 5 minutes.

1

Create an account

Create a free account at seeonwall.com.

Sign up free
2

Add the script tag

After logging in, copy the snippet from your dashboard and paste it into the <head> section of your shop.

<script
  src="https://seeonwall.com/seeonwall.js"
  data-shop-id="YOUR_SHOP_ID"
></script>
3

Add buttons to product pages

Place a .seeonwall-button element wherever you want a preview button to appear. The snippet injects a styled "See on wall" button inside it automatically.

<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"></div>

Installation

Paste this snippet into the <head> of every page where the preview button should appear.

<script
  src="https://seeonwall.com/seeonwall.js"
  data-shop-id="YOUR_SHOP_ID"
  data-lang="en"
></script>

Script tag attributes

AttributeRequiredDescription
data-shop-idYesYour shop ID from the admin dashboard. Required.
data-langNoLanguage for the button and visualizer UI. Accepted values: en, pl, de, es, fr, it, pt, nl, sv, nb, da. When omitted, the snippet reads the page's <html lang> attribute and updates automatically when it changes — no extra configuration needed for SPA language switchers.

Button Markup

Any element with the class seeonwall-button becomes a mount point. The snippet injects a styled "See on wall" button inside it.

Minimal example

<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"></div>

With multiple sizes

When data-poster-sizes contains more than one size, the visualizer shows a row of size pills on the finished preview — on desktop and on the phone alike. When data-poster-width and data-poster-height are also present, they determine the initially selected size (nearest match by area). If omitted, the smallest size is selected by default.

<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-title="Abstract Mountains"
     data-poster-width="50"
     data-poster-height="70"
     data-poster-sizes="30x40,50x70,70x100"
     data-product-page-url="https://example.com/products/abstract-mountains"></div>

Units and paper formats

Any dimension can state its own unit — "24cm", "10in", '11"' — or name an ISO format: "a4" in data-poster-width is 21cm, and in data-poster-height is 29.7cm, so writing it in both declares an A4 poster. Sizes lists take the same forms per entry, so one list may mix them. Shoppers see each size labelled the way you wrote it.

<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="18in"
     data-poster-height="24in"
     data-poster-sizes="12x16in,18x24in,a3"></div>

A shop-wide default unit

data-size-unit sets the unit for values that do not state one. It can also go on the <script> tag to cover the whole shop. It never overrides a value that spelled out its own unit.

<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-size-unit="in"
     data-poster-width="18"
     data-poster-height="24"
     data-poster-sizes="12x16,18x24,24x36"></div>

Framed prints

Previews are unframed by default, matching the bare print most shops sell. If the price on your page includes a frame, set data-frame-default-cm so the preview opens framed. Shoppers can add or remove a frame themselves either way. Shopify and WooCommerce merchants set this in the app block's Framing section and the plugin's Framing settings respectively, rather than editing markup. Preview links have the same option on the create form — tick “Set default frame”.

<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"
     data-frame-default-cm="2"
     data-frame-default-color="#3E2723"></div>

Your own frames

If you have set up a frame catalogue in your SeeOnWall dashboard, name one and the preview opens with it — your name, your width, your colour. This takes priority over data-frame-default-cm and data-frame-default-color.

<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"
     data-frame-preset="Oak"></div>

The name is the key. It has to match a frame in your catalogue exactly, apart from letter case. Rename a frame in the dashboard and this markup stops matching it, so change both together. If a shopper picks a different frame variant on your page, rewrite this attribute and an open preview follows along — until the shopper picks a frame of their own, which always wins.

Mockup product images

If your product photo is a mockup — the print on a coloured background, or inside a frame — the preview shows that whole photo at your poster's size, so the print looks smaller than it will be on the wall. Set data-poster-inset to the rectangle the print actually occupies, as left,top,width,height percentages of the image (e.g. 14.07,23.6,70.07,50.2). Because mockups are usually generated from a template, one value normally covers your whole catalogue. Leave it off if your images are the artwork itself. Shopify merchants set it per product in the Poster artwork inset metafield; WooCommerce merchants draw it on the product's SeeOnWall tab, under Artwork area. On Shopify the inset only applies when the Poster image metafield is set, and on WooCommerce when a poster image is chosen on that tab — the rectangle is measured against that specific image, and the storefront's own product image can change (Shopify swaps it per selected variant).

<div class="seeonwall-button"
     data-poster-url="https://example.com/poster-mockup.jpg"
     data-poster-width="50"
     data-poster-height="70"
     data-poster-inset="14.07,23.6,70.07,50.2"></div>

Per-button language

Add data-lang to an individual button element to pin it to a specific language, independent of the global snippet language. Both the button label and the visualizer opened from it will use that language.

<div class="seeonwall-button"
     data-lang="pl"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"></div>

Button shape

By default the button is filled with our own colours. data-button-variant changes its shape: "outline" keeps the same box with no fill, "glyph" is an icon and a label with no box at all, and "icon" is the icon on its own. The shapes without a fill take their colour from the text around them, so they follow your theme, including in dark mode. With "icon", the button text is still used as the tooltip and as the label screen readers read out. The colour attributes still apply wherever a shape can use them: data-text-color paints the label and the mark — and the outline’s border, unless data-border-color says otherwise — while data-bg-color reaches the solid button alone.

no attribute — the default
data-button-variant="outline"
data-button-variant="glyph"
data-button-variant="icon"

Those four buttons are real, injected by the same snippet this page documents. Click any of them to open the preview on a print from our demo shop.

<!-- An icon and a label, with no box around them -->
<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"
     data-button-variant="glyph"></div>

<!-- The icon on its own, beside your Add to cart button -->
<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"
     data-button-variant="icon"></div>

<!-- The mark and the label in your own colour -->
<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"
     data-button-variant="glyph"
     data-text-color="#2f5d50"></div>

Match your theme's button

Instead of setting a font, size, corners and spacing by hand, point the button at one your theme already has. data-match-button="auto" finds your Add to cart button and copies its shape and typography. Pass a CSS selector instead of "auto" to name a specific button.

<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"
     data-match-button="auto"></div>

<!-- Or name the button to copy, when "auto" picks the wrong one -->
<div class="seeonwall-button"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"
     data-match-button=".my-theme-add-to-cart"
     data-match-colors="true"></div>

Colours are not copied unless you add data-match-colors="true". By default the button keeps your own colour settings, so it reads as a secondary action rather than a second Add to cart.

Button element attributes

AttributeRequiredDescription
data-poster-urlYesAbsolute URL of the poster image. Required.
data-poster-titleNoPoster title shown in the visualizer header.
data-poster-widthYesPoster width in data-size-unit (centimetres by default). Required unless data-poster-sizes is set. When used alongside data-poster-sizes, determines the initially selected size.
data-poster-heightYesPoster height in data-size-unit (centimetres by default). Required unless data-poster-sizes is set. When used alongside data-poster-sizes, determines the initially selected size.
data-size-unitNoUnit for data-poster-width, data-poster-height and data-poster-sizes: "cm" (default) or "in".
data-poster-sizesNoComma-separated sizes, each a WxH pair or a format name: 30x40cm, 18x24in, a4. Enables the size selector. Commas separate entries, so use a period for decimals — 21x29.7cm, not 21x29,7cm.
data-sizes-fromNoReads the sizes from a size control already on the page instead of from this element. Use "auto" to find the control inside the product form, or a CSS selector naming it, e.g. select[name="size"]. Options count as sizes when they read like 60x40cm, 18x24in or A4. It satisfies the size requirement on its own, overrides data-poster-sizes and the width/height pair whenever a control is read, and follows the shopper's selection.
data-frame-default-cmNoWidth in centimetres of the frame the preview starts with, e.g. 2. Omitted or 0 previews the poster unframed. Set it only when the listed price includes a frame.
data-frame-default-colorNoColour of that default frame as a 6-digit hex value, e.g. #3E2723. Ignored unless data-frame-default-cm is greater than 0. Defaults to black.
data-frame-presetNoName of a frame from your SeeOnWall frame catalogue, e.g. Oak. The preview opens with that frame. Takes priority over the two attributes above. An unknown name is ignored.
data-poster-insetNoWhere the artwork sits inside the product image, as left,top,width,height percentages, e.g. 14.07,23.6,70.07,50.2. Use it when the image is a mockup with padding or a frame so the print previews at its true size. Omitted, or if the value is malformed, the whole image is used.
data-product-page-urlNoExplicit product page URL. When set, bookmark links point here instead of the page where the button was clicked. Useful on listing or search pages.
data-langNoPins this button to a specific language (en, pl, de, es, fr, it, pt, nl, sv, nb, or da). Overrides the global snippet language for this button only — the label and the visualizer UI both use this language. Buttons with data-lang are unaffected by SeeOnWall.setLanguage() calls and <html lang> changes.
data-button-text-enNoCustom button label for English. Used when the effective language for this button (from data-lang on this element or the global snippet language) is en.
data-button-text-plNoCustom button label for Polish. Used when the effective language for this button (from data-lang on this element or the global snippet language) is pl.
data-button-text-deNoCustom button label for German. Used when the effective language for this button (from data-lang on this element or the global snippet language) is de.
data-button-text-esNoOptional. Button text for Spanish-speaking visitors. Falls back to the English value.
data-button-text-frNoOptional. Button text for French-speaking visitors. Falls back to the English value.
data-button-text-itNoOptional. Button text for Italian-speaking visitors. Falls back to the English value.
data-button-text-ptNoOptional. Button text for Portuguese-speaking visitors. Falls back to the English value.
data-button-text-nlNoOptional. Button text for Dutch-speaking visitors. Falls back to the English value.
data-button-text-svNoOptional. Button text for Swedish-speaking visitors. Falls back to the English value.
data-button-text-nbNoOptional. Button text for Norwegian-speaking visitors. Falls back to the English value.
data-button-text-daNoOptional. Button text for Danish-speaking visitors. Falls back to the English value.
data-button-variantNoThe button’s shape: solid (default), outline, glyph (icon and text) or icon (icon only). The shapes without a fill ignore data-bg-color and take their colour from the page.
data-bg-colorNoButton background colour. Accepts any CSS color or gradient, e.g. #1a1a2e or linear-gradient(135deg, #c05a33, #9f441f). Overrides the page-level --seeonwall-btn-bg CSS variable for this button only. Only the solid shape has a background to paint; see data-button-variant.
data-text-colorNoButton label text colour, e.g. #ffffff. Overrides --seeonwall-btn-color for this button only.
data-border-radiusNoButton corner radius, e.g. 8px or 50%. Overrides --seeonwall-btn-radius for this button only.
data-border-colorNoButton border colour, e.g. #333333. Overrides the default border colour for this button only.
data-button-widthNoSet to "full" to stretch the button to the full width of its container. Any other value, or none at all, leaves it at its natural width.
data-match-buttonNoCopy the appearance of another button on the page. "auto" finds your Add to cart button; any other value is treated as a CSS selector.
data-match-colorsNoWith data-match-button, also copy that button's colours. Off by default.

The button reads its data-poster-* attributes at click time, so you can update them live (e.g. when a shopper picks a size) and the next click opens the preview with the new values. The Shopify app uses this to follow the selected size variant automatically.


npm package

For storefronts that build their own product pages — Hydrogen, Next.js, Nuxt. If your shop is a Shopify theme or a WooCommerce site, install the app or the plugin instead; you do not need this package.

npm install seeonwall

React

import { useSeeOnWall, SeeOnWallButton } from 'seeonwall/react'

function ProductPage({ product }) {
  useSeeOnWall({ shopId: 'YOUR_SHOP_ID' })

  return (
    <SeeOnWallButton
      posterUrl={product.image}
      posterTitle={product.title}
      posterWidth={50}
      posterHeight={70}
      posterSizes={['30x40', '50x70', '70x100']}
    />
  )
}

useSeeOnWall loads the widget and stops it when the last component using it unmounts. It counts its callers, so calling it in several components is safe.

SeeOnWallButton renders the mount and lets the widget place the button inside it, so branding, allowed domains, theme matching and the button label behave exactly as they do with the script tag. Its props mirror the data attributes above, and posterSizes also accepts an array.

Without React

import { load } from 'seeonwall'

await load({ shopId: 'YOUR_SHOP_ID' })

Load the widget yourself and write the button markup exactly as shown above.

Server rendering is safe. Every function returns quietly when there is no document, so importing the package on the server does not throw.

Source code, the full API and TypeScript types: github.com/seeonwall/seeonwall-npm


JavaScript API

After the snippet loads, a global SeeOnWall object is available on the page.

// Open the visualizer for a poster
SeeOnWall.open({
  posterUrl: 'https://example.com/poster.jpg',
  posterTitle: 'Abstract Mountains',    // optional
  posterWidth: '50cm',                  // optional — number, "24cm"/"10in", or "a4"
  posterHeight: '70cm',                 // optional — same forms; "a4" here is 29.7cm
  posterSizes: '30x40,50x70,70x100',   // optional — enables size selector
  posterInset: '14.07,23.6,70.07,50.2', // optional — artwork rect in a mockup image
  productPageUrl: 'https://example.com/products/abstract-mountains', // optional — bookmark target
  sizeUnit: 'cm',                       // optional — fallback unit for values that state none
  frameDefaultCm: 2,                    // optional — preview starts framed (default: unframed)
  frameDefaultColor: '#3E2723'          // optional — colour of that default frame
})

// Close the visualizer modal (desktop only)
SeeOnWall.close()

// Change the snippet language at runtime (SPA use case)
SeeOnWall.setLanguage('pl')

// Current snippet version
console.log(SeeOnWall.version)

// Storefront catalogue-preview readiness (a cached hint; refresh before acting)
await SeeOnWall.ready()   // start-up is deferred to DOMContentLoaded
console.log(SeeOnWall.isSessionReady())
await SeeOnWall.refreshSessionState()
const unsubscribe = SeeOnWall.on('session-change', () => {
  // Update catalogue-preview controls in this tab. Also runs when the shopper
  // returns after setting up their wall on a phone.
})
// Call unsubscribe() when your page/component is disposed
SeeOnWall.open(params)

Opens the visualizer for the given poster. On desktop it appears in a modal overlay. On mobile it opens in a new tab.

SeeOnWall.close()

Closes the visualizer modal. No-op on mobile.

SeeOnWall.openCataloguePreview(params)

Opens the fast wall preview for one product, on the wall the shopper has already set up. Takes the same poster parameters as SeeOnWall.open() and resolves to { opened: true }, or to { opened: false, reason } when nothing opened. Needs a paid plan. Fast wall preview

SeeOnWall.setLanguage(lang)

Changes the snippet's active language at runtime. Updates all already-injected button labels immediately and applies to every modal opened afterwards. Useful in SPAs when you want to explicitly drive the language from your own logic rather than relying on the automatic <html lang> observer.

SeeOnWall.version

Read-only string with the version of the loaded snippet.

SeeOnWall.ready()

Resolves once the snippet has initialised and the rest of this API is safe to call — button scanning waits for DOMContentLoaded, so the global can exist before it is ready. Await it before reading session state. If you inject the script yourself (async, or through a tag manager), wait for its load event first: window.SeeOnWall does not exist until the script evaluates.

SeeOnWall.isSessionReady()

Returns the cached readiness hint for the current shop session. Use it to decide whether to show optional fast wall preview controls; it is never authorization. It is also false while the snippet is still initialising, so await SeeOnWall.ready() before reading a false as “no wall”.

SeeOnWall.refreshSessionState()

Checks the current session with SeeOnWall and resolves to none, creating, ready, or expired. A missing or expired session is cleared from storefront storage.

SeeOnWall.on('session-change', listener)

Runs the listener when the session state changes in this tab, in another tab on the same shop origin, or when the shopper returns to the page after setting up their wall on a phone — the snippet re-checks a pending session on return. Returns an unsubscribe function.


Fast wall preview

Once a shopper has set up their wall, they can see any other poster on it in one tap — from a product listing, a category grid, or a row of recommendations — without going through the visualizer again. Included in Basic, Premium and Ultra.

Both platform integrations offer this without code. In the Shopify app, switch on the placements you want under Fast wall preview, and the theme extension puts the control on those listings. In the WooCommerce plugin, do the same under Button → Fast wall preview; the plugin marks each eligible product card itself, so no theme selector is needed. Anywhere else it is for shops that load the script tag themselves and call the launcher from their own listing templates.

How it works

  1. 1A shopper opens the visualizer once, from any product, and sets up their wall. That wall belongs to the session and lasts seven days.
  2. 2On your listing pages you ask the snippet whether a wall is ready, and show a small preview control on each product card when it is.
  3. 3The shopper taps the control and the poster appears on their own wall right there — one product, no camera step, no setup.
  4. 4The way out of the preview is a link to that product’s page.
// Show your preview controls only while the shopper has a wall
await SeeOnWall.ready()

function refreshControls() {
  const ready = SeeOnWall.isSessionReady()
  document.querySelectorAll('.my-preview-icon')
    .forEach(el => { el.hidden = !ready })
}

refreshControls()
SeeOnWall.on('session-change', refreshControls)

// Launch one product — the same poster parameters as SeeOnWall.open()
icon.addEventListener('click', async () => {
  const result = await SeeOnWall.openCataloguePreview({
    posterUrl: 'https://example.com/poster.jpg',
    posterTitle: 'Abstract Mountains',                                 // optional
    posterWidth: '50cm',
    posterHeight: '70cm',
    posterSizes: '30x40,50x70,70x100',                                 // optional
    posterInset: '14.07,23.6,70.07,50.2',                              // optional
    productPageUrl: 'https://example.com/products/abstract-mountains'  // optional
  })

  if (!result.opened) {
    // Nothing opened. result.reason says why — see the list below.
    console.log(result.reason)
  }
})

When it can open

  • The shopper has a wall set up for your shop, and that session has not expired.
  • Your plan is Basic, Premium or Ultra.
  • The call carries a poster image URL and a readable width and height — the same values your buttons use.
  • The page calling it is on your allowed-domains list.
  • Your poster views for this hour and this month are not used up.

What comes back

openCataloguePreview() resolves to { opened: true } when a preview opened, or to { opened: false, reason } when nothing did. The reason tells your page what to do instead:

  • no-ready-sessionThe shopper has no wall for your shop yet. Show your ordinary “See on wall” button, which starts one.
  • session-expiredThe wall passed its seven days and has been deleted. The next full preview starts a new one.
  • not-entitledThe plan of this shop does not include the fast wall preview.
  • rate-limitedThe poster views for this hour or this month are used up. They reset at the top of the hour and at the start of the month.
  • invalid-poster-dataThe call had no poster image URL, or no width and height that could be read.
  • poster-unavailableThe preview could not be prepared for this product.
  • network-errorThe request did not reach seeonwall.com, or the snippet had not initialised yet — await SeeOnWall.ready() first.

What the preview does

One product, read-only

The preview shows the poster you passed and nothing else: no poster history, no search, no way to swap to another product. Its main action takes the shopper to your product page.

Opens in place

A modal over your page on a desktop, a nearly full-screen sheet on a phone. It never opens a new tab, because the wall is already set up and there is no camera step to hand over.

Sizes and mockups carry over

The size list and the artwork rectangle inside a mockup image are read exactly as they are on your buttons, so a shopper can still compare sizes inside the preview.

Counts as a poster view

Each fast wall preview is one poster view against your hourly and monthly allowance. One shopper seeing one poster counts once, however often they open it.

Counted separately in your dashboard

Shop analytics shows fast wall previews beside your poster views, with a 30-day chart, and the top-poster table shows how many of each poster’s previews came from a listing.


Language & Localisation

The snippet adapts its button labels and visualizer UI to the visitor's language. Three complementary mechanisms handle this, in priority order.

Automatic: <html lang> observer

When data-lang is not set on the <script> tag, the snippet reads document.documentElement.lang at startup and watches it via a MutationObserver. Most i18n libraries (including react-i18next) update <html lang> automatically — if yours does, no extra configuration is needed.

// Keep <html lang> in sync with your i18n library (e.g. react-i18next)
import i18n from './i18n'

i18n.on('languageChanged', (lang) => {
  document.documentElement.lang = lang
})

// The snippet detects the change automatically — no SeeOnWall calls needed.

Once SeeOnWall.setLanguage() is called, the <html lang> observer is disabled for the rest of the page load. The explicit call becomes authoritative.

Explicit: SeeOnWall.setLanguage(lang)

Call setLanguage(lang) to change the language imperatively at any point after the snippet loads. All already-injected button labels update immediately, and every modal opened afterwards uses the new language.

// Drive language from your own logic
SeeOnWall.setLanguage('pl')

// All injected button labels update immediately.
// The <html lang> observer is disabled for the rest of this page load.
// Call setLanguage() again at any time to switch back.

Static: data-lang on the script tag

Set data-lang="en" or data-lang="pl" on the <script> tag to lock the language for the entire page. The <html lang> observer is never activated. You can still call setLanguage() at runtime to override it.

Per-button language: data-lang on the element

Add data-lang to an individual .seeonwall-button element to pin that specific button to a language, independent of the global snippet setting. The button label and the visualizer opened from that button will both use the specified language. These buttons are unaffected by SeeOnWall.setLanguage() calls and <html lang> mutations — useful for static sites that serve content in multiple languages from a single page.

<div class="seeonwall-button"
     data-lang="pl"
     data-poster-url="https://example.com/poster.jpg"
     data-poster-width="50"
     data-poster-height="70"></div>

Priority order

setLanguage() overrides the current language at any time. The <html lang> observer is only active when neither data-lang was set nor setLanguage() has been called yet.

Per-button text overrides

Individual buttons can also override the label text using data-button-text-en, data-button-text-pl, data-button-text-de, data-button-text-es, data-button-text-fr, data-button-text-it, data-button-text-pt, data-button-text-nl, data-button-text-sv, data-button-text-nb, or data-button-text-da. The attribute matching the effective language for that button (set by data-lang on the element or the global snippet language) is used. Button Markup



Visitor Flow

How a shop visitor experiences seeonwall.com, from clicking the preview button to seeing their poster on their actual wall. Automatic wall detection

Desktop (default)

  1. 1Visitor clicks “See on wall” on a product page.
  2. 2A modal opens on the desktop with a QR code.
  3. 3The visitor sets up the wall background: scans the QR code with their phone to capture the room live, uploads a photo from their PC, or picks a room template.
  4. 4For photo capture and uploads: the wall is detected automatically and its real-world dimensions estimated, so the preview usually appears with no further input. If the photo is unclear, the visitor adjusts pre-filled corners and dimensions instead of starting from scratch.
  5. 5The preview appears straight away — the poster is placed centred on the wall automatically, at its true printed size.
  6. 6The desktop modal shows the same preview, and the poster stays movable: the visitor drags, resizes or reframes it on the finished preview itself.

Same-device (visitor is already on mobile)

  1. 1Visitor taps “See on wall” on their phone.
  2. 2The visualizer opens in a new browser tab — no QR code step.
  3. 3The photo is analysed on arrival: usually the preview appears immediately, otherwise the visitor confirms the pre-filled calibration first.
  4. 4The final preview is shown in the same tab.

If the visitor closes and reopens the modal within the session’s 7-day validity window, the previous session is resumed automatically — they do not have to repeat the photo and calibration steps.


Automatic Wall Detection

Every wall photo is analysed the moment it arrives, before the visitor is asked to do anything. When the analysis is confident — the common case — the poster appears on the wall straight away. Marking corners by hand is the fallback, not the default path.

What happens to a wall photo

  1. 1Quality and content check. Every photo is screened as it arrives. A photo that fails the content check is refused: it is never linked to a session, never shown, and the uploaded file is deleted immediately.
  2. 2Wall detection. The wall is located in the image and its four corners are returned as percentages of the photo, so they survive any resizing.
  3. 3Dimension estimate. The wall's real-world width and height are estimated from the scene, so the visitor is never asked to measure their wall. Where no estimate is possible, a fallback assumes a standard 250 cm wall height and derives the width from the detected shape.
  4. 4One-step setup. When corner confidence is high enough, the calibration and a centred poster placement are saved together in a single step, and the session goes straight from photo to finished preview.
  5. 5The visitor stays in control. The preview is labelled as an automatic estimate and offers a fine-tune action. Corners, wall dimensions, and poster position all remain editable.

When the photo is difficult

Partially hidden wall

A sofa, a shelving unit, or tight framing can hide the wall's real boundary. Instead of giving up, detection returns the largest usable rectangle — the clear area above a couch, for example — and the poster is placed within it.

Low confidence

Below the confidence threshold the visitor gets the manual calibration step, but the corner handles start on the detected positions and the estimated dimensions are pre-filled. It is an adjustment, not a blank slate.

No result at all

If the analysis times out or fails, the flow continues with default corner handles and manual dimensions. Detection is best-effort and never blocks a preview from being created.

Guidance the visitor sees

  • Dim lighting — suggests switching on more light or opening the curtains.
  • Backlit wall or glare, such as a window behind the wall — suggests shooting from a different angle.
  • No wall found — asks for a retake with the wall clearly visible, while still allowing manual placement.
  • Poster larger than the detected wall — the visitor is told the maximum size their wall setup fits and offered the sizes that do, instead of being shown a preview that could not be true.

What makes a good wall photo

  • Frame the whole wall, including where it meets the floor and the ceiling.
  • Shoot straight on rather than at a steep angle.
  • Avoid a bright window directly behind the wall.
  • Turn the lights on — dim photos produce a weaker estimate.

Automatic detection runs on every wall photo, on every plan including the free one. There is nothing to switch on and no setting to configure. Room-scene templates skip the step entirely, because their geometry is already known.

The analysis is performed by Google Vertex AI (Gemini) inside the EU, pinned to europe-west1 through Google's EU Data Boundary endpoint. Wall photos are never used to train a model, and the analysis is deleted together with the session. Sub-processors


Rate Limits

Limits apply per account. Every limit below is hourly, resetting at the top of each UTC hour, except the monthly poster-view ceiling, which resets at the start of each UTC month. Upgrade your plan in the admin dashboard to unlock higher limits.

PlanSessions / hrPhotos / hrPoster views / hrPoster views / moDownloads / hr
FREE502010050025
BASIC5002002,5005,000250
PREMIUM2,5001,00010,00025,0001,000
ULTRA10,0002,00050,000Unlimited2,000
S
A session is created each time a visitor opens the visualizer from your shop.
P
A photo is counted each time a visitor uploads a wall image from their phone.
V
A poster view is counted each time the visualizer renders a poster preview on a calibrated wall. A fast wall preview counts as a poster view as well.
M
Poster views are the one metric with two limits, and both are tracked: a view counts against the hourly budget and against the monthly one, and either being used up stops further views from being recorded. The hourly window resets at the top of each UTC hour, the monthly window at the start of each UTC month.
D
A download is counted each time a visitor saves or shares the final poster preview image.

An IP-level guard applies on top of plan limits: at most 600 session-creation and 300 photo-upload requests per IP address, counted in a rolling one-hour window that starts with that IP's first request. This protects against single-IP abuse and does not affect normal usage patterns.


When Rate Limits Are Hit

What happens from the visitor’s point of view when your shop reaches a limit.

Session-creation limit reached

The “See on wall” button continues to appear on your product pages. When a visitor clicks it, the visualizer modal opens but shows a friendly error message instead of a QR code. The message asks them to try again shortly. No crash, no blank screen.

Photo-upload limit reached

A visitor who has already scanned the QR code and is on the mobile upload screen will see an error if the limit is exceeded between the time they opened the modal and the time they attempt to upload. They are prompted to try again.

Poster view limit reached

A poster view is counted when a preview is shown on a calibrated wall — on the desktop, on the phone, or both. It counts once per session per day however many times it is rendered, so a shopper looking at one preview costs one view. This is the limit a shopper meets at the last step: instead of the poster on their wall they see a message asking them to try again shortly. Poster views are the one metric with two limits, hourly and monthly, and a view spends both.

Monthly poster-view limit reached

Separate from the hourly view limit and enforced alongside it. Where the hourly one clears by itself within the hour, this one stays exhausted until the month rolls over — so it is the limit that means your plan has been outgrown. Until it resets or you upgrade, shoppers who reach the final step are asked to try again later instead of seeing their preview. Everything else on your storefront is unaffected: the button still appears and still opens.

Download limit reached

If the download limit is reached, the save/share action fails and the visitor is shown an error. The preview itself remains visible — only the download action is blocked.

Hourly limits reset at the top of every UTC hour, so traffic spikes shorter than an hour are typically self-resolving. The monthly poster-view ceiling resets at the start of each UTC month — reaching that one is a signal to upgrade rather than something that clears on its own.


Session Lifecycle

Every visualizer session moves through four states. The desktop receives live session updates over a server-sent event stream and reacts to each state change instantly. When wall detection is confident, a session moves from Active to Ready in one step and never enters Calibrated. Sessions expire after 7 days and are then automatically deleted.

CREATED
Created

Triggered when a shop visitor opens the visualizer. A unique session ID is generated and a QR code is displayed on the desktop so the visitor can continue on their phone.

ACTIVE
Active

Triggered when the visitor uploads a photo of their wall from the mobile device. The desktop transitions out of the QR-code screen.

CALIBRATED
Calibrated

Records the wall calibration: four corner coordinates plus the real-world wall dimensions (width and height in cm). The corners are pre-filled by automatic detection and the dimensions estimated, so this state captures what the visitor confirmed or adjusted. Coordinates are stored as percentages so they survive image resizing.

READY
Ready

Reached as soon as the wall is calibrated: a centred poster placement is written with it, so no separate confirmation step stands between the wall and the preview. This is the terminal state — the desktop renders the final canvas overlay. Later edits to the poster update that placement without changing the state.

Fast path: Active → Ready

When corner detection is confident enough, the calibration and a centred placement are written in a single atomic step, so the session skips Calibrated altogether. Room-scene templates take the same path. The desktop then receives one Ready event rather than two separate updates.

Expiry & cleanup

All sessions are kept for 7 days from creation, then permanently deleted regardless of their state. All associated wall photos are removed at the same time. Nothing from the session survives expiry.

Data objects

Sessionunique UUID, current state, shop reference, creation timestamp, 7-day expiry
Photowall image captured or uploaded by the visitor; stored in cloud storage, linked to the session, and accompanied by the analysis derived from it (detected corners, estimated dimensions, quality flags)
Calibrationfour wall-corner coordinates (each 0–100 % of the photo) and real-world wall dimensions (cm)
Placementposter position, scale, and rotation angle on the calibrated wall surface

Poster Image Requirements

Requirements for the poster image URL you provide. The browser loads the image directly into the preview, and the server fetches it separately when generating a downloadable or shareable preview.

  • The URL must be absolute (starting with https://).
  • The image must be publicly accessible — no authentication, no login walls, no hotlink protection that blocks foreign origins.
  • Use JPEG, PNG, or WebP. AVIF displays in the in-browser preview but is not supported for preview download and sharing.
  • For best rendering quality use the largest available version of the image. The visualizer scales it down to fit the wall; low-resolution sources look blurry at large print sizes.
  • No CORS headers are required. The poster is rendered as a plain image overlay rather than drawn onto a canvas, so a cross-origin image from your CDN loads normally.
  • The image must be 4 MB or smaller. The server fetches your poster image during preview generation and rejects anything larger.

The server rejects URLs that do not return a Content-Type starting with image/ (e.g. a page returning text/html will fail). Standard image CDN URLs always satisfy this requirement.

Hotlink protection is the usual culprit when an image will not load: it blocks requests carrying a foreign referrer, which breaks both the in-browser preview and the server-side fetch used for downloads and shares. Disable it for the poster image path, or serve those images from a host that allows it.


WooCommerce hooks & filters

The WooCommerce plugin places and styles the button from its own settings screen. These filters and helpers take over where that screen cannot reach — a theme with its own product template, or a printed size that differs from the shipping dimensions. Add them in your theme’s functions.php or in a small site plugin.

Placing the button yourself

Set Position on the product page to “Place it myself with a shortcode”, which stops the plugin adding a button of its own, then write the shortcode wherever your theme or page builder allows. On a page that is not a product page, name the product by ID.

[seeonwall_button]

[seeonwall_button id="123"]

Published products only — an ID naming a draft or pending product renders nothing. The shortcode works under any position setting, but under the others it adds a second button rather than moving the first, so pick “Place it myself” when the shortcode is meant to be the only one.

Filter reference

seeonwall_button_hook( string $hook, string $position ) — the WooCommerce action the button is rendered on. Return an empty string for no automatic placement at all.
seeonwall_button_priority( int $priority, string $position ) — the priority the button is added at on that action, for moving it within a hook other things also use.
seeonwall_button_html( string $html, WC_Product $product ) — the button markup itself, after the plugin has escaped every attribute value.
seeonwall_poster_width_cm( float|null $cm, WC_Product $product ) — the poster width in centimetres, already converted from your store’s dimension unit.
seeonwall_poster_height_cm( float|null $cm, WC_Product $product ) — the poster height in centimetres, mirroring the width filter.
// Render on an action the settings list does not offer.
add_filter( 'seeonwall_button_hook', function () {
    return 'my_theme_after_gallery';
} );

// Or: no automatic placement at all, leaving the shortcode to place it.
add_filter( 'seeonwall_button_hook', '__return_empty_string' );

Add the two placement filters before wp_loaded — a theme’s functions.php is early enough. The plugin resolves its placement on wp_loaded precisely so a theme can get there first.

Overriding the printed size

By default the poster is measured from WooCommerce’s own product Dimensions, converted from your store’s dimension unit. When the printed sheet is not what you ship, these two filters replace the numbers per product. Return null for a product with no printable size and the button does not appear on it.

add_filter( 'seeonwall_poster_width_cm', function ( $cm, $product ) {
    $printed = $product->get_meta( 'print_width_cm' );
    return '' !== $printed ? (float) $printed : $cm;
}, 10, 2 );

Calling the button from a template

mount_html() returns the same markup the settings screen configures, for any product you hand it.

if ( class_exists( 'SeeOnWall_Storefront' ) ) {
    echo SeeOnWall_Storefront::mount_html( $product );
}

It returns an empty string when the store is not connected or the product is not eligible. It does not load the viewer script by itself, and the plugin enqueues that script on product pages only — so anywhere else, use the shortcode, which handles both.


Allowed Domains

By default, the visualizer accepts session-creation requests from any origin. You can restrict this to a specific list of domains to prevent your Shop ID from being used on other websites.

How to configure

Open the admin dashboard → your shop → Allowed Domains. Add each domain where your embed is installed (e.g. example.com). Subdomains must be added separately. Wildcards are not supported.

What happens when a domain is blocked

If a visitor opens the visualizer from an origin that is not on your list, they see a “not allowed” error screen instead of the QR code. The button still appears on the page — only the session creation is blocked.

Example entries

example.com
www.example.com
shop.example.com

Both example.com and https://example.com are accepted — the protocol is stripped and only the hostname is stored. Up to 20 domains can be added.

We recommend adding your shop domain to the list. Without it, anyone who finds your Shop ID can embed the visualizer on their own website and consume your plan’s rate limit.


Content Security Policy

If your shop sets a Content-Security-Policy header, you need to whitelist seeonwall.com for the snippet to load and the visualizer to embed correctly.

Required CSP directives

script-src https://seeonwall.comAllow the snippet script to load.
frame-src https://seeonwall.comAllow the visualizer iframe to embed.
connect-src https://seeonwall.comAllow the snippet to read your branding setting from the API.
style-src 'unsafe-inline'Allow the stylesheet the snippet injects for the button and modal. Only needed if your policy sets style-src at all.

The visualizer's own API calls happen inside the seeonwall.com iframe, so they don't count against your shop page's connect-src — but the snippet itself makes one call from your page, to https://seeonwall.com/api/shops/{id}/domains, to read your branding setting. Blocked by CSP, that call fails closed and the SeeOnWall logo stays on the button even on plans that hide it. Your poster image URL is loaded inside the iframe context, so you do not need to add your image host to the shop page's img-src directive.

Minimal example header value

Content-Security-Policy: script-src 'self' https://seeonwall.com; frame-src https://seeonwall.com; connect-src 'self' https://seeonwall.com; style-src 'self' 'unsafe-inline';

SPA & Dynamic Pages

The snippet works automatically on single-page applications and any page that adds .seeonwall-button elements dynamically after the initial load.

After the initial DOM scan, the snippet installs a MutationObserver on the document body. Whenever new .seeonwall-button elements are added to the DOM — whether by a React render, a route change, or any other dynamic update — the observer detects them and injects the preview button immediately.

No special setup is needed. You do not need to call SeeOnWall.init() again after navigation events or re-renders. Buttons that were already injected are tracked and will not be processed a second time.


Troubleshooting

Common issues and how to resolve them.

The “See on wall” button does not appear on the page.

Check that: (1) the script tag is in the page <head> with a valid data-shop-id; (2) the mount element has exactly the class seeonwall-button (case-sensitive); (3) data-poster-url is an absolute URL; (4) data-poster-width and data-poster-height are set, or data-poster-sizes is used. Open the browser console for warnings prefixed with [SeeOnWall].

Clicking the button opens the modal but it immediately shows an error.

Likely causes: (a) your allowed-domains list does not include the current page’s origin — add it in the admin dashboard; (b) your hourly session limit has been reached — wait for the next UTC hour or upgrade your plan; (c) the data-shop-id is wrong — copy it again from the dashboard. Allowed Domains

The QR code appears but the mobile page says “session not found” or “session expired”.

Sessions are valid for 7 days. The visitor may have scanned an old QR code from a previous visit. Closing and reopening the modal generates a fresh session with a new QR code. Also confirm the visitor’s phone has a working internet connection.

The final preview looks blurry or the poster image does not render.

The poster image URL must be publicly accessible and not blocked by hotlink protection — check your CDN or shop platform's image settings. Also make sure the source image resolution is high enough for the selected poster size. Image Requirements

The browser console shows a Content-Security-Policy error.

Add script-src, frame-src and connect-src for https://seeonwall.com to your shop's CSP header, and allow the snippet's injected stylesheet. See the Content Security Policy section for the full example. CSP / CORS

The button appears but clicking does nothing in local development.

If your allowed-domains list is non-empty, add http://localhost:PORT to it while developing. An empty list (the default) allows all origins including localhost. Allowed Domains


Testing & Staging

Use the staging environment to test your integration without affecting production data.

Staging URLs

Replace the production URL with the staging URL when testing:

<script
  src="https://staging.seeonwall.com/seeonwall.js"
  data-shop-id="YOUR_STAGING_SHOP_ID"
></script>

The visualizer iframe loads from: https://staging.seeonwall.com/visualizer/embed

Getting a staging Shop ID

Create a separate account at staging.seeonwall.com/admin. Staging accounts and production accounts are completely independent — data never crosses between environments.

Isolated from production

Sessions created on staging do not count toward your production rate limits or analytics. You can run as many test flows as needed without affecting your shop's usage metrics.

Browser console debugging

The snippet logs useful information to the browser console. Open DevTools and look for:

  • [SeeOnWall] — snippet lifecycle events (script loaded, button injected, modal opened/closed)
  • postMessage events between your page and the visualizer iframe — inspect them with: window.addEventListener('message', e => console.log(e))
  • Session hint stored in localStorage on your shop's own origin, one entry per shop — inspect it at: localStorage.getItem('seeonwall:session:YOUR_SHOP_ID'). The JSON includes sessionId, expiresAt, state (creating or ready), and optionally referenceSize. It is a UI hint, not authorization.
  • Network tab — the snippet calls /api/shops/* from your page, and the iframe calls /api/sessions/* and /api/storage/*; check response codes if the flow stalls

Privacy & GDPR

seeonwall.com is designed to be privacy-friendly by default: no shopper accounts, no tracking cookies, and nothing kept for longer than 7 days.

  • No user accounts. Visitors use the visualizer without signing up or logging in.
  • No tracking cookies. The snippet does not set any cookies on your shop page. It stores one localStorage entry on your shop's origin — the current session ID, expiry, readiness hint and optional comparison size — and the visualizer stores the same on the seeonwall.com origin, so visitors can resume an in-progress session without repeating the photo step.
  • Wall photos are temporary. Each photo uploaded is stored only for the duration of the session (maximum 7 days) and deleted automatically when the session expires.
  • Sessions auto-delete. All session data — including calibration coordinates and poster placement — is permanently deleted on expiry. There is no archive or backup.
  • No personal identifiers. Sessions are identified by a random UUID. No email address, name, IP address, or device fingerprint is linked to session data.
  • Wall photos are analysed by AI. Each photo passes through Google Vertex AI (Gemini) to locate the wall, estimate its size, and screen for inappropriate content. The analysis runs inside the EU (europe-west1, via Google’s EU Data Boundary endpoint), photos are never used to train models, and the analysis is deleted along with the session.

If your shop operates under GDPR, you may want to add a brief mention to your privacy policy noting that the wall-preview feature is powered by seeonwall.com, that temporary wall photos are stored for up to 7 days before automatic deletion, and that each photo is analysed by Google Vertex AI as a sub-processor. All processing happens inside the EU; no data is transferred outside it. Sub-processors