Widget Embedding Guide

Add the chat assistant to your store with a single line of code

Overview

The Emporiqa chat widget is a lightweight chat assistant that you can embed on your e-commerce store. It provides:

  • 24/7 customer support
  • Product search and recommendations
  • Image search: shoppers upload a photo and get matching products
  • In-chat cart: add, update, and check out inside the conversation (opt-in)
  • Optional order tracking (if configured in store settings)
  • Proactive messages based on visitor behavior
  • Post-chat satisfaction rating (CSAT)
  • Conversion tracking: chat to cart to purchase attribution
  • Multi-language support
  • Responsive design (works on desktop and mobile)
  • Customizable appearance to match your brand
  • Optional human handoff for complex queries

One line of code: The widget is embedded with a single script tag. No complex setup or build process required.

Platform Support: We have ready-to-use integrations for Drupal Commerce, WooCommerce, Sylius, Magento 2, PrestaShop, and Shopware. The widget works with any website or e-commerce platform.

Getting Your Store ID

Each store has a unique Store ID that identifies it. To find your Store ID and embed code:

  1. Log in to your Emporiqa dashboard
  2. Navigate to Store Settings → Integration tab
  3. Find your Store ID in the header and the ready-to-use Embed Code in the Chat Widget Integration section
  4. Click the copy button to copy either the Store ID or the complete embed code

Quick Tip: The embed code is ready to use - just copy and paste it into your store's HTML.

Important: Before embedding the widget, ensure you have sent your product and page data via webhooks. The widget won't function properly without product data.

Basic Embedding

Add this single line of code to your website, just before the closing </body> tag:

<script async crossorigin="anonymous" src="https://emporiqa.com/chat/embed/?store_id=YOUR_STORE_ID&language=en"></script>

Replace YOUR_STORE_ID with your actual Store ID, and en with your store's language code (e.g., en, de, fr, pt-br, zh-cn). Both 2-letter codes and full regional codes are accepted. Codes are case-insensitive: EN, en-US, and en-us all work.

Cross-language support: The language parameter sets the language of your store content (products, pages). If a customer writes in a different language, the chat automatically detects it and responds in their language while searching your content in its original language.

Required Parameters: Both store_id and language are required. The widget will not load without them.

Optional Parameters

You can add optional parameters to control currency display and sales channel filtering:

<script async crossorigin="anonymous" src="https://emporiqa.com/chat/embed/?store_id=YOUR_STORE_ID&language=en&currency=EUR&channel=b2b"></script>
Parameter Required Description
store_id Yes Your store's unique identifier
language Yes Language code for the widget (e.g., en, de, pt-br). Determines the display language and which product translations to search.
currency No ISO 4217 currency code (e.g., EUR, USD). When set, product prices are shown in this currency. Products without a matching price entry use the first available price as fallback.
channel No Sales channel identifier (e.g., b2b, retail). When set, only shows products and pages from this channel plus store-wide items.
attribution No off stops the chat from setting the emporiqa_sid cookie and reporting purchases, so no order is linked to a conversation. The Shopware plugin sets it from its Link orders to chats switch, which also stops it sending orders.
user_id No HMAC-signed customer token for order tracking. See User Identification.

Example

<!DOCTYPE html>
<html>
<head>
    <title>My Store</title>
</head>
<body>
    <!-- Your store content -->

    <!-- Emporiqa Widget - place before </body> -->
    <script async crossorigin="anonymous" src="https://emporiqa.com/chat/embed/?store_id=ABC123xyz...&language=en"></script>
</body>
</html>

The script automatically creates a floating chat button in the corner of your page. Customers can click it to open the chat widget.

User Identification

You can optionally pass a user_id parameter to track conversations across sessions and devices. This is useful for logged-in customers. If you use order tracking, the user ID is forwarded to your order tracking API so you can restrict results to that customer's orders.

The user_id parameter must be a signed token generated server-side using your connection secret. This prevents customers from forging another user's identity.

Token Format

The token is composed of two parts separated by a dot: a Base64url-encoded JSON payload and an HMAC-SHA256 signature.

base64url({"uid":"CUSTOMER_ID","ts":UNIX_TIMESTAMP}).hmac_sha256_hex(payload, webhook_secret)
  • uid - Any string that uniquely identifies the customer (customer ID, email, account number)
  • ts - Required. The time the token was created, as an integer Unix timestamp in seconds (not milliseconds), at most 5 minutes in the future. Tokens expire after 7 days, so generate a fresh one on each page render rather than caching it.

Generating the Token (Server-Side)

The token must be generated on your server - never expose the connection secret to the browser.

PHP

function emporiqa_generate_user_token(string $userId, string $webhookSecret): string
{
    $payload = json_encode(['uid' => $userId, 'ts' => time()], JSON_UNESCAPED_SLASHES);
    $encodedPayload = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
    $signature = hash_hmac('sha256', $encodedPayload, $webhookSecret);

    return $encodedPayload . '.' . $signature;
}

Python

import base64, hashlib, hmac, json, time

def emporiqa_generate_user_token(user_id: str, webhook_secret: str) -> str:
    payload = json.dumps({"uid": user_id, "ts": int(time.time())}, separators=(",", ":")).encode()
    encoded_payload = base64.urlsafe_b64encode(payload).decode().rstrip("=")
    signature = hmac.new(webhook_secret.encode(), encoded_payload.encode(), hashlib.sha256).hexdigest()
    return f"{encoded_payload}.{signature}"

Embedding with the Token

<script async crossorigin="anonymous" src="https://emporiqa.com/chat/embed/?store_id=YOUR_STORE_ID&language=en&user_id=TOKEN_HERE"></script>

Drupal Commerce (PHP)

$params = ['store_id' => $store_id, 'language' => $language];
if ($user->isAuthenticated()) {
    $params['user_id'] = emporiqa_generate_user_token((string) $user->id(), $webhook_secret);
}
$url = 'https://emporiqa.com/chat/embed/?' . http_build_query($params);

Important: Never expose your connection secret in client-side code. The token must be generated on the server and injected into the page template.

Tip: If you don't need user identification, omit the user_id parameter. The chat widget works without it. The customer's user ID will not be available for order tracking in that case.

Customization

Customize the widget appearance through your Emporiqa dashboard. No code changes required!

Available Options

Option Description Default
Custom CSS Custom styles for the widget (see below) Default theme
Position Widget button location Bottom Right
Widget Title Header text (supports {store_name}) {store_name}
Greeting Message Initial welcome message Hi! How can I help you today?
Chat picture Square image shown in the chat header and next to the chat's answers. PNG, JPEG or WebP, no animation. Not shown on the round chat button. None
Tone Standard, Friendly, Professional or Concise. Changes wording only, not what the chat may claim or recommend. Applies to the store, not per language. Standard
Allowed Domains Restrict where widget can be embedded All domains

Custom CSS

Use the Custom CSS field to fully customize the chat widget's appearance. Your CSS is injected into the widget iframe and can override any styles.

CSS Variables

The widget uses CSS variables for colors. Override them in the :root selector:

:root {
  --primary-color: #1A73E8;    /* Send button and accent color */
  --secondary-color: #1669D4;  /* Hover states */
}

Example: Custom Brand Colors

:root {
  --primary-color: #E91E63;    /* Pink brand color */
  --secondary-color: #C2185B;  /* Darker pink for hover */
}

/* Additional customizations */
.btn-primary {
  border-radius: 4px;  /* Square corners */
}

Position Options

Bottom Right
Bottom Left
Top Right
Top Left

Multi-Language Settings

You can configure different titles and messages for each language in your dashboard. The widget automatically detects the customer's language.

Store Settings & Display Options

These options live under Platform > Store Settings and control what the assistant shows to shoppers. They change the display only; your synced product data is never modified.

Setting What it does Default
AI-enhanced descriptions Shows an AI-written summary on the product detail card. When off, shoppers see your original description. Off (new stores)
In-chat cart Lets shoppers add, update, and check out inside the chat. Also needs the cart handler on your page (see Cart Handler); the platform plugins set that up for you. Off
Stock indicator Shows the stock count and in-stock / out-of-stock state on product cards. Hiding it is display only; the cart still rejects out-of-stock items on the server. On
Tax-included and excluded prices Shows both the tax-included and tax-excluded price on product cards. Off by default, since most shoppers want the single final price. Off
Voice mode Lets shoppers talk to the chat instead of typing and hear its answers read aloud. The widget asks the shopper's browser for microphone permission only when they tap the microphone. A conversation in which the shopper speaks costs $0.15 more, once. Under Widget appearance. Off
Browsing context Lets the widget read standard page metadata (OpenGraph, schema.org) so the assistant can reference the product a shopper is viewing and has recently browsed. The page a shopper is on when they send a message is saved with that message, so your team can see it in the conversation; query parameters are removed except those that identify the product, and it is deleted with the conversation after 90 days. Uncheck to stop collecting any browsing data. On

Show part of a description, keep the rest for search

Add a <!--more--> marker anywhere in a product description to split it. Everything before the marker is shown to shoppers; everything after it is used only to help the assistant answer questions, which is handy for FAQ, care, or sizing detail you do not want on the card. The full description is always indexed for search either way. Drupal's <!--break--> works the same way. This needs an editor that keeps HTML comments; some rich-text editors strip them on save.

Satisfaction rating (CSAT)

After a conversation, shoppers can rate it with a thumbs up or down. The score and the top reasons for negative ratings appear in your dashboard. This is always on.

Responsive Behavior

Desktop

  • Floating button (56x56px)
  • Expandable chat window
  • Can be maximized to fullscreen
  • Window size: ~50% width, ~80% height

Mobile

  • Full-screen experience
  • Touch-optimized interface
  • Native keyboard handling

The widget automatically adapts to screen size. On screens smaller than 850px, it switches to mobile mode.

Security

Allowed Domains

Restrict where your widget can be embedded by configuring allowed domains in your dashboard. This prevents unauthorized sites from using your widget.

Example: If you set allowed domains to mystore.com, shop.mystore.com, the widget will only work on those domains.

Chat by Country

If you only sell to certain countries, you can limit where the chat appears. Under Settings › Security, choose either Only these countries or All except these countries and list the ones you want.

Visitors outside the countries you serve never see the chat button at all, and they cannot start a conversation, so they never appear on your bill. Everyone else is unaffected.

How it works: the visitor's country is worked out from their connection by our network layer, the same way a CDN does it. Only the country is used, never a city or an exact location, and the country code is kept in their browser so it does not need to be looked up on every page.

Worth knowing: this is a way to cut traffic that was never going to buy from you, not a security control. Anyone using a VPN appears to be somewhere else. And if a visitor's country cannot be established, the chat is shown rather than hidden, so a real customer is never turned away by mistake.

Changes take up to 10 minutes to reach every page, because the widget script is cached.

Token Authentication

The widget uses time-limited, HMAC-signed tokens for API authentication. This prevents abuse and ensures requests come from legitimate widget instances.

Rate Limiting

Built-in rate limiting protects against abuse:

  • 10 messages per minute per IP address
  • 100 messages per hour per conversation

Load on Click

Some data protection officers will not have a visitor's browser contact a third party before the visitor asks for it, because the request itself carries their IP address. For them, load the chat only when the visitor clicks (the "two-click solution"): your page draws the chat button itself, and nothing is loaded from Emporiqa until the click.

Copy the snippet from Settings › Integration › Embed code and webhook › Load the chat only when the visitor clicks and paste it instead of the script tag, just before </body>. Your colours, corner and store name are already filled in. It is self-contained: a style, a button with the icon drawn inline, and a short script. Its shape:

<style>#emporiqa-load{ ... your colours and corner ... }</style>
<button type="button" id="emporiqa-load" aria-label="Your store"
  data-store="YOUR_STORE_ID"
  data-src="https://emporiqa.com/chat/embed/?store_id=YOUR_STORE_ID"><svg>...</svg></button>
<script> ... </script>

What happens

  • Before the click: no request to Emporiqa (no script, no settings, no font), no cookie, and nothing written to the browser's storage. The button reads one entry, emporiqaChatOpened_<store id>, which exists only if this visitor opened the chat before.
  • On the click: the chat loads and opens, and the real chat button takes the place of yours.
  • On later pages: for 30 days after the visitor last opened the chat, it loads straight away, without a click, so the conversation carries on as they browse.
  • When your store cannot chat (switched off, over its monthly cap, or not offered in the visitor's country): the button still shows, because finding out would mean asking us before the click, and it disappears when clicked.
  • Language: the chat follows the page's <html lang>. To fix it, add &language=de to data-src.

What is off in this mode

Proactive messages, the button's attention pulse, the daily counts of how many visitors saw and opened the chat, and the list of recently viewed products. Conversations, the cart, order status and handoff to your team work as usual, and where page awareness is on, the page the visitor is on still goes with their message. Your dashboard's visitor and open counts leave out these visitors; their conversations are counted.

After the click the chat still sets the emporiqa_sid cookie, which links an order to the conversation for your dashboard's revenue from chat. The chat works without it. To store nothing beyond the chat, switch off Link orders to chats in the Shopware plugin (version 1.4.0 or newer), which then sends no orders either, or add &attribution=off to data-src, which stops the cookie. Our other plugins still send each order to Emporiqa from your server for now.

Behind your consent banner

To load the chat once a visitor has given consent, keep the snippet and call window.EmporiqaLoad() from your consent manager. It loads the chat in the same mode without opening it. The page receives emporiqa:ready on window when the chat is there (then window.EmporiqaChat.open() opens it), or emporiqa:unavailable when there is no chat to show; the button removes itself on either. If a visitor withdraws consent, remove emporiqaChatOpened_<store id> from localStorage too, or later pages keep loading the chat for 30 days after their last open.

What is stored after the click

On your store's domain, and nothing else. <id> is your store ID, followed by the customer's ID when the tag carries a signed user_id. None holds a name, email address or IP address.

Entry Where Purpose Lifetime
emporiqaChatThreadId_<id> local The conversation, so it carries on from page to page Until the visitor starts a new conversation or clears site data
emporiqaChatOpened_<id> local When the visitor last opened the chat, so later pages load it at once Used for 30 days after the last open; stays until site data is cleared
emporiqaHasUnread_, emporiqaLastMsgTime_, emporiqaOpenedThread_, emporiqaOpenedAt_, emporiqaNoSession_, emporiqaLastPoll_, emporiqaPollMs_ <id> local The unread badge, and how often to check for a reply Overwritten as the chat is used
emporiqaLeader_, emporiqaChatOpenAt_, emporiqaBroadcast_ <id> local Which open tab checks for replies, so several tabs do not all ask (emporiqaBroadcast_ only in browsers without BroadcastChannel) Seconds; refreshed while a tab is open
emporiqaCsatPending_, emporiqaCsatAsked_ <id> local The satisfaction question after a conversation, asked once (only where it is on) Until the next conversation
emporiqaUnavailable_<id> local Your store cannot take a conversation right now, so the button is not shown 3 minutes
emporiqa_country local The visitor's country code (only where you limit the chat by country) 30 days, or 1 hour where the chat is not offered there
emporiqaChatIsOpen_, emporiqaWasLeader_ <id> session Whether the chat panel is open, and which tab checks for replies Until the tab closes
emporiqa_sid cookie Links an order to the conversation, for your plugin. SameSite=Lax, Secure on https. Not set when linking orders to chats is off 1 hour, renewed while the chat is used

The chat window itself runs on Emporiqa's domain, and the browser keeps its storage separate for each shop. It holds the conversation and the time of its last message, which signed-in customer a conversation belongs to, how often the satisfaction question was shown, and, where voice is on, whether the visitor confirmed the voice notice and whether spoken replies are muted.

Not written in this mode: emporiqaVisited_, emporiqaVisit_, emporiqaTriggerClaim_, emporiqaReported_, emporiqaRecentlyViewed_ and emporiqaNudged. The full list for both modes is on the cookies page.

Cart Handler

In-chat cart is off by default. Turn it on under Platform > Store Settings (see Store Settings). Once enabled, the widget delegates cart actions (add, remove, update, clear, view, checkout) to a JavaScript function on your page. The plugins for Drupal Commerce, WooCommerce, Sylius, Magento 2, PrestaShop, and Shopware implement this function automatically.

For custom platforms, register EmporiqaCartHandler before the widget loads:

window.EmporiqaCartHandler = async function({ action, items }) {
  const item = items?.[0];

  if (action === 'add') {
    const res = await fetch('/api/cart/add', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        product_id: item.product_id,
        variation_id: item.variation_id,
        quantity: item.quantity
      })
    });
    return await res.json();
  }

  if (action === 'view') {
    const res = await fetch('/api/cart');
    return await res.json();
  }

  if (action === 'checkout') {
    return { success: true, checkoutUrl: '/checkout' };
  }

  // handle remove, update, clear similarly
};

Response Format

Every handler call must resolve with this structure:

{
  "success": true,
  "checkoutUrl": "/checkout/2",
  "cart": {
    "items": [
      {
        "product_id": "257",
        "variation_id": "1097",
        "name": "iPhone 14 - Midnight, 128GB",
        "quantity": 1,
        "unit_price": 699.00,
        "image_url": "https://store.com/images/iphone-14.jpg",
        "product_url": "https://store.com/product/iphone-14"
      }
    ],
    "item_count": 3,
    "total": 1397.00,
    "currency": "USD"
  }
}
Field Type Description
success boolean Whether the operation succeeded
checkoutUrl string URL for the checkout page (used by the "Proceed to checkout" button)
cart.items[].unit_price number Price per single unit as a plain number (e.g. 699.00, not "$699.00")
cart.item_count integer Total quantity across all items
cart.total number Cart total as a plain number
cart.currency string ISO 4217 currency code (e.g. USD, EUR)

Important: All prices must be plain numbers, not formatted strings. 699.00 is correct. "$699.00", "699.000000", and "&#36;699.00" are all wrong. The widget handles formatting based on the product's currency and the widget's language setting.

Troubleshooting

Widget button doesn't appear

  • Check that the chat widget is enabled in Store Settings → Integration
  • Check that the script is placed before </body>
  • Verify your Store ID is correct
  • Check browser console for JavaScript errors
  • Ensure your subscription is active
  • If your site sends a Content-Security-Policy header, it must allow https://emporiqa.com in script-src, connect-src and frame-src. The script reads your chat settings with a request, so without connect-src the chat does not appear

Widget shows 'No products synced' error

  • Send your product data via webhooks before using the widget
  • Check that products were successfully synced in your dashboard
  • Ensure the language parameter matches the language of your synced products

Chat responses are slow or not loading

  • Check your store has an active subscription
  • Verify you haven't exceeded your conversation limit (Emporiqa accounts)
  • Check for network/firewall issues

No microphone button, or voice will not start

  • Check that Voice mode is switched on under Store Settings → Widget appearance
  • If your site sends a Permissions-Policy header, it must allow the microphone for the chat frame, for example microphone=(self "https://emporiqa.com"). A policy of microphone=() blocks voice on every page it covers
  • The shopper has to allow the microphone when their browser asks. If they refused it once, they can change it in the browser's site settings
  • Voice needs your store to be served over HTTPS
  • Voice works in current Chrome, Safari, Firefox and Edge, on phones and computers

Widget blocked on my domain

  • Check 'Allowed Domains' setting in your dashboard
  • Add your domain to the allowed list
  • Include all variations (www, non-www, subdomains)

Widget conflicts with other scripts

  • The widget uses isolated CSS and JavaScript
  • Try loading the script with defer attribute
  • Check for CSS z-index conflicts

Testing Your Widget

Before going live, test your widget thoroughly:

  1. Ask about products in your catalog
  2. Test product search with various queries
  3. Ask about policies (shipping, returns, etc.)
  4. Test order tracking (if configured): "Where is my order #12345?"
  5. Test on different devices (desktop, tablet, mobile)
  6. Test in different browsers (Chrome, Firefox, Safari)
  7. Verify customization settings are applied

Need Help with Integration?

Stuck somewhere in the integration? Email us with what you're trying to do and what's not working, and we'll help.

Contact Support