Viewer – Two-Way Communication via postMessage

Introduction

Viewer embeds communicate with the host page through postMessage. The current runtime starts with a nonce handshake, then requests an embed token using ALTER_TOOL_INIT_SESSION.

Setup:

  1. In the Alter Product app, open the E-commerce Settings Embeding panel.
  2. Add the domain where the Viewer will be embedded (for example https://yourwebsite.com or http://localhost:3000) and save.

Flow:

  1. Your website loads the Viewer iframe.
  2. The iframe sends ALTER_CHILD_HELLO; your page answers with ALTER_PARENT_ACK and the same nonce.
  3. The iframe requests a token with ALTER_TOOL_INIT_SESSION and payload.tool set to viewer.
  4. Your backend creates the token through Alter Product Public API, and your page responds with ALTER_TOOL_SESSION_READY.
  5. With add_to_cart=1, the Viewer can emit ALTER_VIEWER_ADD_TO_CART. The host can also request the current payload with ALTER_VIEWER_GET_PRODUCT_DATA and receive ALTER_VIEWER_DATA_RESPONSE.

Variant metadata configured in Product Configuration is included in the payload.

Messages sent by the Viewer

TypeDescription
ALTER_CHILD_HELLOHandshake init (contains a nonce)
ALTER_TOOL_INIT_SESSIONRequests a session token from the host website
ALTER_VIEWER_ADD_TO_CARTUser clicked “Add to Cart” in a Viewer embed with add_to_cart=1
ALTER_VIEWER_DATA_RESPONSEResponse to product data request
ALTER_WP_RUNTIME_CONTEXT_REQUESTWordPress/WooCommerce runtime context request
ALTER_WP_LOCAL_DESIGN_REQUESTWordPress local design bootstrap request when wp_local_design=1

Messages received by the Viewer

TypeDescription
ALTER_PARENT_ACKHandshake confirmation (must include the nonce)
ALTER_TOOL_SESSION_READYProvides session token to authorize the Viewer
ALTER_TOOL_SESSION_ERRORToken request failed (host-side error)
ALTER_VIEWER_GET_PRODUCT_DATARequests product data from the Viewer
ALTER_WP_RUNTIME_CONTEXT_RESPONSEWordPress/WooCommerce runtime context response
ALTER_WP_LOCAL_DESIGN_RESPONSEWordPress local design bootstrap response

Full Example (HTML + JS)

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="UTF-8" />
5  <title>Alter Product Viewer Communication (Handshake + Token + postMessage)</title>
6  <meta name="viewport" content="width=device-width, initial-scale=1" />
7  <style>
8    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
9    iframe { width: 100%; height: 600px; border: 0; display: block; border-radius: 12px; }
10    .row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
11    button { padding: 10px 14px; border-radius: 10px; border: 1px solid #ddd; background: #fff; cursor: pointer; }
12    button:disabled { opacity: 0.5; cursor: not-allowed; }
13    pre { background: #0b1020; color: #d7e1ff; padding: 12px; border-radius: 12px; overflow:auto; }
14  </style>
15</head>
16<body>
17  <h1>Viewer — Two-Way Communication via postMessage</h1>
18
19  <div class="row">
20    <button id="btnGetData" disabled>Request Current Payload</button>
21  </div>
22
23  <iframe
24    id="viewerWidget"
25    src="https://alterproduct.com/app/viewer/1?nav=0&add_to_cart=1"
26    title="Alter Product Viewer"
27    allowfullscreen>
28  </iframe>
29
30  <h3>Logs</h3>
31  <pre id="log"></pre>
32
33  <script>
34    const IFRAME_ORIGIN = 'https://alterproduct.com';
35    const iframe = document.getElementById('viewerWidget');
36    const logEl = document.getElementById('log');
37    const btnGetData = document.getElementById('btnGetData');
38
39    let handshakeOk = false;
40    let tokenReady = false;
41
42    function log(...args) {
43      const line = args.map(a => (typeof a === 'string' ? a : JSON.stringify(a, null, 2))).join(' ');
44      logEl.textContent += line + '\n';
45    }
46
47    function postToIframe(payload) {
48      iframe.contentWindow.postMessage(payload, IFRAME_ORIGIN);
49    }
50
51    /**
52     * Your backend MUST request a session token (recommended).
53     * This endpoint is owned by YOU and should call Alter Product API using server-side credentials.
54     * Expected response: { sessionToken: "..." }
55     */
56    async function getSessionTokenFromYourBackend({ tool, mode, uiDesignId }) {
57      const url = new URL('/api/alter/session-token', window.location.origin);
58      url.searchParams.set('tool', tool);
59      url.searchParams.set('mode', mode || 'design');
60      url.searchParams.set('uiDesignId', String(uiDesignId || 0));
61
62      const res = await fetch(url.toString(), { method: 'GET' });
63      if (!res.ok) throw new Error('Failed to get session token');
64      const data = await res.json();
65      if (!data || !data.sessionToken) throw new Error('Missing sessionToken');
66      return data.sessionToken;
67    }
68
69    // UI: request product data after the iframe has received a session token.
70    btnGetData.addEventListener('click', () => {
71      if (!tokenReady) return;
72      log('[Host] -> Viewer: ALTER_VIEWER_GET_PRODUCT_DATA');
73      postToIframe({ type: 'ALTER_VIEWER_GET_PRODUCT_DATA' });
74    });
75
76    // Handle messages from the Viewer (iframe)
77    window.addEventListener('message', async (event) => {
78      // 1) Validate origin
79      if (event.origin !== IFRAME_ORIGIN) return;
80
81      // 2) Validate source (must be the embedded iframe)
82      if (event.source !== iframe.contentWindow) return;
83
84      const msg = event.data || {};
85      if (!msg.type || typeof msg.type !== 'string') return;
86
87      // -----------------------
88      // A) HANDSHAKE
89      // Viewer -> Host: ALTER_CHILD_HELLO { nonce }
90      // Host   -> Viewer: ALTER_PARENT_ACK { nonce }
91      // -----------------------
92      if (msg.type === 'ALTER_CHILD_HELLO') {
93        const nonce = msg.nonce;
94        if (!nonce || typeof nonce !== 'string') return;
95
96        handshakeOk = true;
97        log('[Viewer] -> Host: ALTER_CHILD_HELLO', { nonce });
98
99        log('[Host] -> Viewer: ALTER_PARENT_ACK');
100        postToIframe({ type: 'ALTER_PARENT_ACK', nonce });
101
102        return;
103      }
104
105      // Ignore everything until handshake is done
106      if (!handshakeOk) return;
107
108      // -----------------------
109      // B) TOKEN INIT (on-demand)
110      // Viewer -> Host: ALTER_TOOL_INIT_SESSION { payload: { tool, mode, uiDesignId } }
111      // Host   -> Viewer: ALTER_TOOL_SESSION_READY { token }
112      // -----------------------
113      if (msg.type === 'ALTER_TOOL_INIT_SESSION') {
114        try {
115          const payload = msg.payload || {};
116          const tool = String(payload.tool || 'viewer').toLowerCase();
117          const mode = String(payload.mode || 'design');
118          const uiDesignId = Number(payload.uiDesignId || 0);
119
120          log('[Viewer] -> Host: ALTER_TOOL_INIT_SESSION', { tool, mode, uiDesignId });
121
122          const token = await getSessionTokenFromYourBackend({ tool, mode, uiDesignId });
123
124          log('[Host] -> Viewer: ALTER_TOOL_SESSION_READY');
125          postToIframe({ type: 'ALTER_TOOL_SESSION_READY', tool, token });
126          tokenReady = true;
127          btnGetData.disabled = false;
128        } catch (e) {
129          log('[Host] -> Viewer: ALTER_TOOL_SESSION_ERROR', String(e && e.message ? e.message : e));
130          postToIframe({ type: 'ALTER_TOOL_SESSION_ERROR', message: String(e && e.message ? e.message : e) });
131        }
132        return;
133      }
134
135      // -----------------------
136      // C) Viewer events
137      // -----------------------
138      if (msg.type === 'ALTER_VIEWER_DATA_RESPONSE') {
139        log('[Viewer] -> Host: ALTER_VIEWER_DATA_RESPONSE', msg.payload);
140        return;
141      }
142
143      if (msg.type === 'ALTER_VIEWER_ADD_TO_CART') {
144        log('[Viewer] -> Host: ALTER_VIEWER_ADD_TO_CART', msg.payload);
145        return;
146      }
147    });
148  </script>
149</body>
150</html>

Example payload

{
  "userDesignId": 10,
  "designName": "Mug 450ml (15oz)",
  "totalPrice": {
    "value": 24.99,
    "currency": "EUR"
  },
  "productGroup": {
    "id": 1,
    "name": {
      "pl": "Kubek 450ml (15oz)",
      "en": "Mug 450ml (15oz)"
    }
  },
  "productItems": [
    {
      "model3d": {
        "id": 3
      },
      "size": {
        "id": 9,
        "name": {
          "pl": "450ml (15oz)",
          "en": "450ml (15oz)"
        },
        "measureSize": {
          "D": 8.65,
          "H": 11.95
        },
        "externalMapping": {
          "attribute": {
            "internalId": 123,
            "slug": "pa_size",
            "label": "Size"
          },
          "term": {
            "internalId": 456,
            "slug": "450ml-15oz",
            "label": "450ml (15oz)"
          }
        }
      },
      "material": {
        "id": 3,
        "name": {
          "pl": "Ceramika",
          "en": "Ceramic"
        },
        "externalMapping": {
          "attribute": {
            "internalId": 124,
            "slug": "pa_material",
            "label": "Material"
          },
          "term": {
            "internalId": 457,
            "slug": "ceramic",
            "label": "Ceramic"
          }
        }
      },
      "printingMethod": {
        "id": 5,
        "name": {
          "pl": "Sublimacja",
          "en": "Sublimation"
        },
        "externalMapping": {
          "attribute": {
            "internalId": 125,
            "slug": "pa_printing-method",
            "label": "Printing method"
          },
          "term": {
            "internalId": 458,
            "slug": "sublimation",
            "label": "Sublimation"
          }
        }
      },
      "color": {
        "id": 11,
        "name": {
          "pl": "Domyślny",
          "en": "Default"
        },
        "hex": "#FFFFFF",
        "customColor": false,
        "pickedColors": {},
        "patternId": null,
        "externalMapping": {
          "attribute": {
            "internalId": 126,
            "slug": "pa_color",
            "label": "Color"
          },
          "term": {
            "internalId": 459,
            "slug": "default",
            "label": "Default"
          }
        }
      },
      "variant": {
        "id": 11,
        "productGroupId": 1,
        "productModel3dId": 3,
        "sizeId": 9,
        "materialId": 3,
        "printingMethodId": 5,
        "colorId": 11,
        "metadata": {
          "sku": "MUG-450-WHITE"
        },
        "minOrderQuantity": 1,
        "processingTime": null,
        "stockQuantity": null,
        "volume": null,
        "weight": null
      },
      "unitPrice": {
        "value": 24.99,
        "currency": "EUR"
      },
      "totalPrice": {
        "value": 24.99,
        "currency": "EUR"
      },
      "quantity": 1
    }
  ]
}