Quick Start
Get up and running in under 5 minutes.
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>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
| Attribute | Required | Description |
|---|---|---|
data-shop-id | Yes | Your shop ID from the admin dashboard. Required. |
data-lang | No | Language 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. |
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 seeonwallReact
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 disposedSeeOnWall.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.versionRead-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
- 1A shopper opens the visualizer once, from any product, and sets up their wall. That wall belongs to the session and lasts seven days.
- 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.
- 3The shopper taps the control and the poster appears on their own wall right there — one product, no camera step, no setup.
- 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-session— The shopper has no wall for your shop yet. Show your ordinary “See on wall” button, which starts one.session-expired— The wall passed its seven days and has been deleted. The next full preview starts a new one.not-entitled— The plan of this shop does not include the fast wall preview.rate-limited— The 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-data— The call had no poster image URL, or no width and height that could be read.poster-unavailable— The preview could not be prepared for this product.network-error— The request did not reach seeonwall.com, or the snippet had not initialised yet — await SeeOnWall.ready() first.
What the preview does
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.
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.
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.
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.
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
Hosted Preview Links
A preview link is a permanent seeonwall.com URL that stands in for the embedded button. It carries one poster and its physical dimensions, so a shopper who opens it gets the full capture → calibrate → preview flow with no script on your site at all. Built for marketplaces that don't allow sellers to inject scripts into listings.
Here's a live one you can open right now — this is exactly what a shopper sees: seeonwall.com/p/a838654a41
How a preview link works
- 1You create the link in your dashboard under “Preview Links”: pick a shop, supply the poster image (upload a file or paste a URL), and enter the real printed width and height in centimetres. A title and a list of available sizes are optional.
- 2We copy the poster into permanent storage and hand you a short URL of the form seeonwall.com/p/{token}. The image no longer depends on your own hosting staying up.
- 3You paste that URL wherever you can put plain text — a marketplace listing description, an email, a social post, a printed card.
- 4A shopper opens it on their phone, photographs their wall, and sees the poster on it at true physical scale. No app, no account, nothing to install.
How many links you get
Preview links are counted per account, not per shop — a multi-shop account draws from one pool. Only enabled links count toward the active limit.
| Plan | Active links | Total (incl. disabled) |
|---|---|---|
| FREE | 10 | 20 |
| BASIC | 500 | 1,000 |
| PREMIUM | 5,000 | 10,000 |
| ULTRA | 50,000 | 100,000 |
Disabled links keep their row and their stored poster image, so there's a second ceiling at twice the active limit covering active and disabled links together. Delete links you no longer need rather than leaving them disabled forever.
Poster image
- Supply the image either as a direct file upload or as a URL we fetch server-side. Exactly one of the two — supplying both, or neither, is rejected.
- JPEG, PNG, or WebP, up to 10 MB. Both paths run through the same validation, which checks the file's magic bytes rather than trusting its declared type.
- A supplied URL must use HTTPS and resolve to a public address. Redirects are not followed, and private, loopback and link-local targets are blocked.
- We re-encode the image and downscale it so its longest edge is at most 2000 px. Send the largest version you have — the visualizer scales down cleanly, but a low-resolution source looks blurry at large print sizes.
- Width and height are the real printed dimensions in centimetres, not the image's pixel dimensions. These are what make the preview accurate, so they matter more than anything else on the form.
What to know before you rely on them
Disabling a link stops it resolving and frees its slot against your active limit, while keeping the URL and its analytics history. Deleting removes the row and the stored poster image permanently — the URL cannot be recovered.
Every visitor who opens a preview link creates a session and uploads a photo, so preview link traffic draws on the same session, photo, view and download limits as the embedded snippet — the hourly ones, and the monthly poster-view ceiling.
A link belongs to one of your shops and inherits its branding, so its traffic also counts toward that shop's analytics. On top of that, every link has its own stats on the Preview Links page: how many times it was opened, how many wall previews came out of it, and a 30-day chart. An open is one visit to the link's page, so a refresh counts again. Per-link stats need a paid plan, but every link is tracked from the day it's created.
There is no embedding page — the visitor is on seeonwall.com directly — so your shop's allowed-domains list is bypassed for verified preview link tokens only. It still guards every other way of creating a session.
When creating a link, you can turn on a watermark and enter your own text — your shop name, for example. It's stamped diagonally into the poster image itself, not just overlaid on screen, so it stays in place in every preview, download and share. Off by default, and it can't be changed once the link is created.
If you sell the print framed, tick “Set default frame” when creating the link and pick a width and colour. The preview then opens with that frame instead of a bare print, so it matches the photo and the price on your listing. Shoppers can still widen it, recolour it or take it off. Up to 10 cm wide, and like the watermark it's fixed once the link is created.
For step-by-step screenshots and per-platform rules on where these links are permitted, see the marketplace guide. → Marketplace setup guide
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)
- 1Visitor clicks “See on wall” on a product page.
- 2A modal opens on the desktop with a QR code.
- 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.
- 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.
- 5The preview appears straight away — the poster is placed centred on the wall automatically, at its true printed size.
- 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)
- 1Visitor taps “See on wall” on their phone.
- 2The visualizer opens in a new browser tab — no QR code step.
- 3The photo is analysed on arrival: usually the preview appears immediately, otherwise the visitor confirms the pre-filled calibration first.
- 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
- 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.
- 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.
- 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.
- 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.
- 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
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.
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.
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.
| Plan | Sessions / hr | Photos / hr | Poster views / hr | Poster views / mo | Downloads / hr |
|---|---|---|---|---|---|
| FREE | 50 | 20 | 100 | 500 | 25 |
| BASIC | 500 | 200 | 2,500 | 5,000 | 250 |
| PREMIUM | 2,500 | 1,000 | 10,000 | 25,000 | 1,000 |
| ULTRA | 10,000 | 2,000 | 50,000 | Unlimited | 2,000 |
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.
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.
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.
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.
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.
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.
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.
Triggered when the visitor uploads a photo of their wall from the mobile device. The desktop transitions out of the QR-code screen.
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.
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.
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.
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
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.
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.
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.comBoth 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.com | Allow the snippet script to load. |
frame-src https://seeonwall.com | Allow the visualizer iframe to embed. |
connect-src https://seeonwall.com | Allow 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.
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].
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
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 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
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
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