Widget Embedding Guide
Add the chat assistant to your store with a single line of code
On This Page
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:
- Log in to your Emporiqa dashboard
- Navigate to Store Settings → Integration tab
- Find your Store ID in the header and the ready-to-use Embed Code in the Chat Widget Integration section
- 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¤cy=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
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=detodata-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 "$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-Policyheader, it must allowhttps://emporiqa.cominscript-src,connect-srcandframe-src. The script reads your chat settings with a request, so withoutconnect-srcthe 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-Policyheader, it must allow the microphone for the chat frame, for examplemicrophone=(self "https://emporiqa.com"). A policy ofmicrophone=()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
deferattribute - Check for CSS z-index conflicts
Testing Your Widget
Before going live, test your widget thoroughly:
- Ask about products in your catalog
- Test product search with various queries
- Ask about policies (shipping, returns, etc.)
- Test order tracking (if configured): "Where is my order #12345?"
- Test on different devices (desktop, tablet, mobile)
- Test in different browsers (Chrome, Firefox, Safari)
- 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