Customizer – Communication via postMessage

Introduction

The Customizer can communicate with your website via postMessage. For security, embedding requires a handshake and a session token.

Setup:

  1. Open the E-commerce Settings tab in the Alter Product app.
  2. In the Integration section, set the domain where the iframe will be embedded (e.g. https://yourwebsite.com or http://localhost:3000).

Flow:

  1. Your website loads the Customizer iframe.
  2. The iframe and your website perform a postMessage handshake (nonce).
  3. The iframe requests a session token from your website (ALTER_TOOL_INIT_SESSION).
  4. Your backend requests the token from Alter Product API and your website responds with ALTER_TOOL_SESSION_READY.
  5. After ALTER_TOOL_READY, the Customizer can send events like ALTER_CUSTOMIZER_ADD_TO_CART.

You can also add metadata keys to each variant in the design — in the Product Configuration tab. These keys will be included in the payload.

Messages sent by the Customizer

TypeDescription
ALTER_CHILD_HELLOHandshake init (contains a nonce)
ALTER_TOOL_INIT_SESSIONRequests a session token from the host website
ALTER_TOOL_READYCustomizer is authorized and ready
ALTER_CUSTOMIZER_ADD_TO_CARTUser clicked “Add to Cart” (payload contains saved design/order data)
ALTER_TOOL_ERRORFatal / authorization / runtime error

When the customer clicks Add to Cart in the Customizer, an order record is created with the default status: shopping_cart. While the order is shopping_cart, the customer can continue editing the design (e.g. using a link like: https://alterproduct.com/app/customizer/{customizerId}/{orderId}). Once the status changes to other than shopping_cart or editable, editing for customer is locked and the customer sees a view version of the design.

Messages received by the Customizer

TypeDescription
ALTER_PARENT_ACKHandshake confirmation (must include the nonce)
ALTER_TOOL_SESSION_READYProvides session token to authorize the Customizer
ALTER_TOOL_SESSION_ERRORToken request failed (host-side error)

Full Example (HTML + JS)

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

Example payload

{
  "userDesignId": 10,
  "designName": "Mug 450ml (15oz)",
  "totalPrice": {
    "value": 0,
    "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"
          }
        }
      },
      "printMethod": {
        "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",
        "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,
        "printTypeId": 5,
        "colorId": 11,
        "metadata": null,
        "minOrderQuantity": null,
        "processingTime": null,
        "stockQuantity": null,
        "volume": null,
        "weight": null
      },
      "unitPrice": {
        "value": 0,
        "currency": "EUR"
      },
      "totalPrice": {
        "value": 0,
        "currency": "EUR"
      },
      "quantity": 1
    }
  ]
}